From b386d51ce42969bae41e540bc714d57cc760b9bb Mon Sep 17 00:00:00 2001 From: William Moore Date: Thu, 14 Mar 2024 22:40:44 +0000 Subject: [PATCH 01/13] Initial save-to-figure functionality --- plugin/omero_iviewer/views.py | 7 +- src/app/context.js | 6 +- src/app/header.html | 5 ++ src/app/header.js | 131 +++++++++++++++++++++++++++++++++- src/utils/constants.js | 6 ++ src/viewers/viewer/Viewer.js | 1 - 6 files changed, 152 insertions(+), 4 deletions(-) diff --git a/plugin/omero_iviewer/views.py b/plugin/omero_iviewer/views.py index 439b96375..b7ce0b5ec 100644 --- a/plugin/omero_iviewer/views.py +++ b/plugin/omero_iviewer/views.py @@ -18,7 +18,7 @@ from django.shortcuts import render from django.http import JsonResponse, Http404 from django.conf import settings -from django.urls import reverse +from django.urls import reverse, NoReverseMatch from os.path import splitext from collections import defaultdict @@ -84,6 +84,11 @@ def index(request, iid=None, conn=None, **kwargs): # we add the (possibly prefixed) uris params['WEBGATEWAY'] = reverse('webgateway') params['WEBCLIENT'] = reverse('webindex') + try: + params['OMERO_FIGURE'] = reverse('figure_index') + except NoReverseMatch: + # omero-figure not installed + pass params['WEB_API_BASE'] = reverse( 'api_base', kwargs={'api_version': WEB_API_VERSION}) if settings.FORCE_SCRIPT_NAME is not None: diff --git a/src/app/context.js b/src/app/context.js index 710f59039..108a18b5e 100644 --- a/src/app/context.js +++ b/src/app/context.js @@ -33,7 +33,7 @@ import { import { APP_NAME, IMAGE_CONFIG_RELOAD, IVIEWER, INITIAL_TYPES, LUTS_NAMES, LUTS_PNG_URL, PLUGIN_NAME, PLUGIN_PREFIX, REQUEST_PARAMS, SYNC_LOCK, - TABS, URI_PREFIX, WEB_API_BASE, WEBCLIENT, WEBGATEWAY + TABS, URI_PREFIX, WEB_API_BASE, WEBCLIENT, WEBGATEWAY, OMERO_FIGURE } from '../utils/constants'; /** @@ -483,6 +483,10 @@ export default class Context { this.prefixed_uris.set( key, typeof this.initParams[key] === 'string' ? this.initParams[key] : '/' + key.toLowerCase())); + // OMERO_FIGURE might not be installed + if (this.initParams[OMERO_FIGURE]) { + this.prefixed_uris.set(OMERO_FIGURE, this.initParams[OMERO_FIGURE]); + } } /** diff --git a/src/app/header.html b/src/app/header.html index 407c8a65f..0fb5ec2e5 100644 --- a/src/app/header.html +++ b/src/app/header.html @@ -55,6 +55,11 @@ Save Projection as new Image +
  • + Save Viewport${context.image_configs.size > 1 ? 's' : ''} as Figure + +
  • diff --git a/src/app/header.js b/src/app/header.js index be836705f..d3746064e 100644 --- a/src/app/header.js +++ b/src/app/header.js @@ -25,7 +25,8 @@ import Misc from '../utils/misc'; import Ui from '../utils/ui'; import {IMAGE_VIEWPORT_CAPTURE} from '../events/events'; import { - APP_TITLE, CSV_LINE_BREAK, INITIAL_TYPES, IVIEWER, PROJECTION, WEBCLIENT + APP_TITLE, CSV_LINE_BREAK, INITIAL_TYPES, IVIEWER, PROJECTION, + WEBCLIENT, OMERO_FIGURE } from '../utils/constants'; import { IMAGE_VIEWER_RESIZE } from '../events/events'; @@ -514,4 +515,132 @@ export class Header { Ui.showModalMessage(msg, 'Close'); } } + + /** + * Check if OMERO.figure is installed + * + * @return {Boolean} + * @memberof Header + */ + omeroFigureIsInstalled() { + return Boolean(this.context.getPrefixedURI(OMERO_FIGURE)); + } + + /** + * Save the current layout of viewers as an OMERO.figure file + * + * @memberof Header + */ + saveAsFigure() { + let figureUrl = this.context.server + this.context.getPrefixedURI(OMERO_FIGURE); + if (!figureUrl) { + console.log("OMERO_FIGURE url not found. OMERO.figure not installed."); + return; + } + + // We need zoom/pan info from each ol3-viewer, but there is no list of these components + // since they are created in the html template to wrap each image_config + // The approach used here is from + // https://discourse.aurelia.io/t/getting-component-state-from-child-class/5380/3 + + const viewers = document.querySelectorAll("ol3-viewer"); + let panels = []; + viewers.forEach((component) => { + let viewModel = component.au["ol3-viewer"].viewModel; + let view = viewModel.viewer.viewer_.getView(); + let image_config = viewModel.image_config; + let image_info = image_config.image_info; + let params = viewModel.viewer.getViewParameters(); + + // Figure "100%" zoom means image fits in viewport + let viewportWidth = viewModel.viewer.viewer_.getViewport().offsetWidth; + let viewportHeight = viewModel.viewer.viewer_.getViewport().offsetHeight; + let zoomSizeX = image_info.dimensions.max_x / view.getResolution(); + let zoomSizeY = image_info.dimensions.max_y / view.getResolution(); + var xZoom = zoomSizeX / viewportWidth; + var yZoom = zoomSizeY / viewportHeight; + var panelZoom = 100 * Math.min(xZoom, yZoom); + + // dx and dy will be 0 if centre hasn't moved + // calculate dx and dy (panning from centre) + var halfWidth = image_info.dimensions.max_x / 2; + var halfHeight = image_info.dimensions.max_y / 2; + let center = view.getCenter(); + var dx = halfWidth - center[0]; + var dy = halfHeight + center[1]; + let channels = image_info.channels; + // figure doesn't know about 'greyscale' so we fake it... + if (image_info.model == "greyscale") { + channels.forEach(ch => { + if (ch.active) { + ch.color = "FFFFFF" + } + }); + } + + let panel = { + x: parseInt(image_config.position.left), + y: parseInt(image_config.position.top), + width: parseInt(image_config.size.width), + height: parseInt(image_config.size.height), + imageId: image_info.image_id, + theZ: params.z, + theT: params.t, + channels, + name: image_info.image_name, + orig_width: image_info.dimensions.max_x, + orig_height: image_info.dimensions.max_y, + sizeZ: image_info.dimensions.max_z, + sizeT: image_info.dimensions.max_t, + // TODO: check use of image_info.image_pixels_size.unit_x and .symbol_x etc + // with images of different units + pixel_size_x: image_info.image_pixels_size.x, + pixel_size_y: image_info.image_pixels_size.y, + pixel_size_z: image_info.image_pixels_size.z, + deltaT: image_info.image_delta_t, + zoom: panelZoom, + dx: dx, + dy: dy, + rotation: 0, + } + panels.push(panel); + }); + + // Scale all panels to fit on default A4 figure. + let minX = panels.reduce((prev, p) => Math.min(prev, p.x), Infinity); + let minY = panels.reduce((prev, p) => Math.min(prev, p.y), Infinity); + let maxX = panels.reduce((prev, p) => Math.max(prev, p.x + p.width), 0); + let figureA4width = 595; + let figureMargin = 20; + let availWidth = figureA4width - (2 * figureMargin); + let scale = availWidth / (maxX - minX); + + panels.forEach(panel => { + panel.x = (scale * (panel.x - minX)) + figureMargin; + panel.y = (scale * (panel.y - minY)) + figureMargin; + panel.width = panel.width * scale; + panel.height = panel.height * scale; + }); + + const figureName = prompt("Enter Figure name"); + if (!figureName) { + return; + } + + let figureJSON = JSON.stringify({ + version: 7, + figureName: figureName, + panels: panels, + }); + + // Save + $.post(figureUrl + "/save_web_figure/", {figureJSON,}) + .done(function( data ) { + // let fileId = +data; + let html = `Figure created: ID "${data}.
    + Open in new tab.`; + + Ui.showModalMessage(html, "OK"); + }); + } } diff --git a/src/utils/constants.js b/src/utils/constants.js index 4f2eb22cf..a41a27ac0 100644 --- a/src/utils/constants.js +++ b/src/utils/constants.js @@ -81,6 +81,12 @@ export const WEBCLIENT = "WEBCLIENT"; */ export const PLUGIN_PREFIX = "PLUGIN_PREFIX"; +/** + * a convenience string lookup for OMERO_FIGURE + * @type {string} + */ +export const OMERO_FIGURE = "OMERO_FIGURE"; + /** * the viewer's dom element prefix (complemented by config id) * @type {string} diff --git a/src/viewers/viewer/Viewer.js b/src/viewers/viewer/Viewer.js index 0a175aef5..bb4aa3656 100644 --- a/src/viewers/viewer/Viewer.js +++ b/src/viewers/viewer/Viewer.js @@ -2301,7 +2301,6 @@ class Viewer extends OlObject { getViewParameters() { if (this.viewer_ === null || this.getImage() === null) return null; var viewProps = this.viewer_.getView().getProperties() - console.log(viewProps) return { "z": this.getDimensionIndex('z'), "t": this.getDimensionIndex('t'), From d8c3ecbb89e65389a98dbfe3e15ce12943dcbb6a Mon Sep 17 00:00:00 2001 From: William Moore Date: Fri, 15 Mar 2024 12:22:27 +0000 Subject: [PATCH 02/13] Move figure code to figure.js --- src/app/header.js | 85 +----------------------------------- src/utils/figure.js | 103 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 83 deletions(-) create mode 100644 src/utils/figure.js diff --git a/src/app/header.js b/src/app/header.js index d3746064e..d2e10a20b 100644 --- a/src/app/header.js +++ b/src/app/header.js @@ -23,6 +23,7 @@ import JSZip from '../../node_modules/jszip/dist/jszip'; import * as TextEncoding from "../../node_modules/text-encoding"; import Misc from '../utils/misc'; import Ui from '../utils/ui'; +import { exportViewersAsPanelsJson } from '../utils/figure'; import {IMAGE_VIEWPORT_CAPTURE} from '../events/events'; import { APP_TITLE, CSV_LINE_BREAK, INITIAL_TYPES, IVIEWER, PROJECTION, @@ -537,90 +538,8 @@ export class Header { console.log("OMERO_FIGURE url not found. OMERO.figure not installed."); return; } - - // We need zoom/pan info from each ol3-viewer, but there is no list of these components - // since they are created in the html template to wrap each image_config - // The approach used here is from - // https://discourse.aurelia.io/t/getting-component-state-from-child-class/5380/3 - - const viewers = document.querySelectorAll("ol3-viewer"); - let panels = []; - viewers.forEach((component) => { - let viewModel = component.au["ol3-viewer"].viewModel; - let view = viewModel.viewer.viewer_.getView(); - let image_config = viewModel.image_config; - let image_info = image_config.image_info; - let params = viewModel.viewer.getViewParameters(); - - // Figure "100%" zoom means image fits in viewport - let viewportWidth = viewModel.viewer.viewer_.getViewport().offsetWidth; - let viewportHeight = viewModel.viewer.viewer_.getViewport().offsetHeight; - let zoomSizeX = image_info.dimensions.max_x / view.getResolution(); - let zoomSizeY = image_info.dimensions.max_y / view.getResolution(); - var xZoom = zoomSizeX / viewportWidth; - var yZoom = zoomSizeY / viewportHeight; - var panelZoom = 100 * Math.min(xZoom, yZoom); - - // dx and dy will be 0 if centre hasn't moved - // calculate dx and dy (panning from centre) - var halfWidth = image_info.dimensions.max_x / 2; - var halfHeight = image_info.dimensions.max_y / 2; - let center = view.getCenter(); - var dx = halfWidth - center[0]; - var dy = halfHeight + center[1]; - let channels = image_info.channels; - // figure doesn't know about 'greyscale' so we fake it... - if (image_info.model == "greyscale") { - channels.forEach(ch => { - if (ch.active) { - ch.color = "FFFFFF" - } - }); - } - let panel = { - x: parseInt(image_config.position.left), - y: parseInt(image_config.position.top), - width: parseInt(image_config.size.width), - height: parseInt(image_config.size.height), - imageId: image_info.image_id, - theZ: params.z, - theT: params.t, - channels, - name: image_info.image_name, - orig_width: image_info.dimensions.max_x, - orig_height: image_info.dimensions.max_y, - sizeZ: image_info.dimensions.max_z, - sizeT: image_info.dimensions.max_t, - // TODO: check use of image_info.image_pixels_size.unit_x and .symbol_x etc - // with images of different units - pixel_size_x: image_info.image_pixels_size.x, - pixel_size_y: image_info.image_pixels_size.y, - pixel_size_z: image_info.image_pixels_size.z, - deltaT: image_info.image_delta_t, - zoom: panelZoom, - dx: dx, - dy: dy, - rotation: 0, - } - panels.push(panel); - }); - - // Scale all panels to fit on default A4 figure. - let minX = panels.reduce((prev, p) => Math.min(prev, p.x), Infinity); - let minY = panels.reduce((prev, p) => Math.min(prev, p.y), Infinity); - let maxX = panels.reduce((prev, p) => Math.max(prev, p.x + p.width), 0); - let figureA4width = 595; - let figureMargin = 20; - let availWidth = figureA4width - (2 * figureMargin); - let scale = availWidth / (maxX - minX); - - panels.forEach(panel => { - panel.x = (scale * (panel.x - minX)) + figureMargin; - panel.y = (scale * (panel.y - minY)) + figureMargin; - panel.width = panel.width * scale; - panel.height = panel.height * scale; - }); + let panels = exportViewersAsPanelsJson(); const figureName = prompt("Enter Figure name"); if (!figureName) { diff --git a/src/utils/figure.js b/src/utils/figure.js new file mode 100644 index 000000000..29e99be6d --- /dev/null +++ b/src/utils/figure.js @@ -0,0 +1,103 @@ +// +// Copyright (C) 2024 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 . +// + +export function exportViewersAsPanelsJson() { + // We need zoom/pan info from each ol3-viewer, but there is no list of these components + // since they are created in the html template to wrap each image_config + // The approach used here is from + // https://discourse.aurelia.io/t/getting-component-state-from-child-class/5380/3 + + const viewers = document.querySelectorAll("ol3-viewer"); + let panels = []; + viewers.forEach((component) => { + let viewModel = component.au["ol3-viewer"].viewModel; + let view = viewModel.viewer.viewer_.getView(); + let image_config = viewModel.image_config; + let image_info = image_config.image_info; + let params = viewModel.viewer.getViewParameters(); + + // Figure "100%" zoom means image fits in viewport + let viewportWidth = viewModel.viewer.viewer_.getViewport().offsetWidth; + let viewportHeight = viewModel.viewer.viewer_.getViewport().offsetHeight; + let zoomSizeX = image_info.dimensions.max_x / view.getResolution(); + let zoomSizeY = image_info.dimensions.max_y / view.getResolution(); + var xZoom = zoomSizeX / viewportWidth; + var yZoom = zoomSizeY / viewportHeight; + var panelZoom = 100 * Math.min(xZoom, yZoom); + + // dx and dy will be 0 if centre hasn't moved + // calculate dx and dy (panning from centre) + var halfWidth = image_info.dimensions.max_x / 2; + var halfHeight = image_info.dimensions.max_y / 2; + let center = view.getCenter(); + var dx = halfWidth - center[0]; + var dy = halfHeight + center[1]; + let channels = image_info.channels; + // figure doesn't know about 'greyscale' so we fake it... + if (image_info.model == "greyscale") { + channels.forEach(ch => { + if (ch.active) { + ch.color = "FFFFFF" + } + }); + } + + let panel = { + x: parseInt(image_config.position.left), + y: parseInt(image_config.position.top), + width: parseInt(image_config.size.width), + height: parseInt(image_config.size.height), + imageId: image_info.image_id, + theZ: params.z, + theT: params.t, + channels, + name: image_info.image_name, + orig_width: image_info.dimensions.max_x, + orig_height: image_info.dimensions.max_y, + sizeZ: image_info.dimensions.max_z, + sizeT: image_info.dimensions.max_t, + // TODO: check use of image_info.image_pixels_size.unit_x and .symbol_x etc + // with images of different units + pixel_size_x: image_info.image_pixels_size.x, + pixel_size_y: image_info.image_pixels_size.y, + pixel_size_z: image_info.image_pixels_size.z, + deltaT: image_info.image_delta_t, + zoom: panelZoom, + dx: dx, + dy: dy, + rotation: 0, + } + panels.push(panel); + }); + + // Scale all panels to fit on default A4 figure. + let minX = panels.reduce((prev, p) => Math.min(prev, p.x), Infinity); + let minY = panels.reduce((prev, p) => Math.min(prev, p.y), Infinity); + let maxX = panels.reduce((prev, p) => Math.max(prev, p.x + p.width), 0); + let figureA4width = 595; + let figureMargin = 20; + let availWidth = figureA4width - (2 * figureMargin); + let scale = availWidth / (maxX - minX); + + panels.forEach(panel => { + panel.x = (scale * (panel.x - minX)) + figureMargin; + panel.y = (scale * (panel.y - minY)) + figureMargin; + panel.width = panel.width * scale; + panel.height = panel.height * scale; + }); +} From f4f0d270b5a04454e1550caa807fd112974c8ad7 Mon Sep 17 00:00:00 2001 From: William Moore Date: Fri, 15 Mar 2024 14:37:37 +0000 Subject: [PATCH 03/13] Handle Shapes: Ellipse and Polygon so far... --- src/utils/figure.js | 58 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/src/utils/figure.js b/src/utils/figure.js index 29e99be6d..68920ae5d 100644 --- a/src/utils/figure.js +++ b/src/utils/figure.js @@ -16,6 +16,47 @@ // along with this program. If not, see . // +import {featureToJsonObject} from '../viewers/viewer/utils/Conversion'; + +function colorIntToHex(signed_integer) { + if (typeof signed_integer !== 'number') return null; + if (signed_integer < 0) signed_integer = signed_integer >>> 0; + var intAsHex = signed_integer.toString(16); + // pad with zeros to have 8 digits (rgba), slice to 6 (rgb) + intAsHex = ("00000000" + intAsHex).slice(-8).slice(0, 6); + return "#" + intAsHex; +} + +function featureToFigureShape(feature) { + let ft = featureToJsonObject(feature); + const shapeType = ft['@type'].split('#')[1]; + let shapeJson = {}; + // {"type":"Ellipse","x":111.24363636363661,"y":178.26909090909086,"radiusX":144.0632573639581,"radiusY":72.03162868197904,"rotation":64.34564318455512,"strokeWidth":1,"strokeColor":"#FFFFFF","id":-4191737437767269} + if (shapeType == "Ellipse") { + shapeJson = { + x: ft.X, + y: ft.Y, + radiusX: ft.RadiusX, + radiusY: ft.RadiusY, + rotation: 0, + strokeWidth: ft.StrokeWidth.Value, + strokeColor: colorIntToHex(ft.StrokeColor), + } + } else if (shapeType == "Polygon") { + // "type":"Polygon","points":"188.0795898437498,182.61894531249982 188.0795898437498,182.61894531249982 186.3558 + shapeJson = { + points: ft.Points, + strokeWidth: ft.StrokeWidth.Value, + strokeColor: colorIntToHex(ft.StrokeColor), + } + } else { + console.log("Feature not converted!", ft); + } + + shapeJson.type = shapeType; + return shapeJson; +} + export function exportViewersAsPanelsJson() { // We need zoom/pan info from each ol3-viewer, but there is no list of these components // since they are created in the html template to wrap each image_config @@ -32,6 +73,7 @@ export function exportViewersAsPanelsJson() { let params = viewModel.viewer.getViewParameters(); // Figure "100%" zoom means image fits in viewport + // viewer_ is the OlMap. let viewportWidth = viewModel.viewer.viewer_.getViewport().offsetWidth; let viewportHeight = viewModel.viewer.viewer_.getViewport().offsetHeight; let zoomSizeX = image_info.dimensions.max_x / view.getResolution(); @@ -40,6 +82,19 @@ export function exportViewersAsPanelsJson() { var yZoom = zoomSizeY / viewportHeight; var panelZoom = 100 * Math.min(xZoom, yZoom); + // Find visible shapes in viewport + var shapes = []; + let vpExtent = viewModel.viewer.viewer_.getView().calculateExtent(); + console.log("vpExtent", vpExtent); + var regions = viewModel.viewer.getRegions(); + console.log('regions', regions); + if (regions) { + regions.forEachFeatureInExtent(vpExtent, function(feature){ + shapes.push(featureToFigureShape(feature)); + }); + console.log("shapes", shapes); + } + // dx and dy will be 0 if centre hasn't moved // calculate dx and dy (panning from centre) var halfWidth = image_info.dimensions.max_x / 2; @@ -81,6 +136,7 @@ export function exportViewersAsPanelsJson() { dx: dx, dy: dy, rotation: 0, + shapes, } panels.push(panel); }); @@ -100,4 +156,6 @@ export function exportViewersAsPanelsJson() { panel.width = panel.width * scale; panel.height = panel.height * scale; }); + + return panels; } From 689a7d8578dccaaf47364fde4875eb3c7d8568d8 Mon Sep 17 00:00:00 2001 From: William Moore Date: Sun, 17 Mar 2024 08:59:09 +0000 Subject: [PATCH 04/13] Add support for Rect, Line, Arrow to figure --- src/utils/figure.js | 43 +++++++++++++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/src/utils/figure.js b/src/utils/figure.js index 68920ae5d..ad32e078b 100644 --- a/src/utils/figure.js +++ b/src/utils/figure.js @@ -29,8 +29,8 @@ function colorIntToHex(signed_integer) { function featureToFigureShape(feature) { let ft = featureToJsonObject(feature); - const shapeType = ft['@type'].split('#')[1]; - let shapeJson = {}; + let shapeType = ft['@type'].split('#')[1]; + let shapeJson; // {"type":"Ellipse","x":111.24363636363661,"y":178.26909090909086,"radiusX":144.0632573639581,"radiusY":72.03162868197904,"rotation":64.34564318455512,"strokeWidth":1,"strokeColor":"#FFFFFF","id":-4191737437767269} if (shapeType == "Ellipse") { shapeJson = { @@ -38,22 +38,42 @@ function featureToFigureShape(feature) { y: ft.Y, radiusX: ft.RadiusX, radiusY: ft.RadiusY, - rotation: 0, - strokeWidth: ft.StrokeWidth.Value, - strokeColor: colorIntToHex(ft.StrokeColor), + rotation: 0 } } else if (shapeType == "Polygon") { // "type":"Polygon","points":"188.0795898437498,182.61894531249982 188.0795898437498,182.61894531249982 186.3558 shapeJson = { - points: ft.Points, - strokeWidth: ft.StrokeWidth.Value, - strokeColor: colorIntToHex(ft.StrokeColor), + points: ft.Points + } + } else if (shapeType == "Rectangle") { + // {"type":"Rectangle","x":119.15636363636364,"y":143.36,"width":193.62909090909093,"height":82.85090909090908,"strokeWidth":1,"strokeColor":"#FFFFFF","id":-967897903885322}, + shapeJson = { + x: ft.X, + y: ft.Y, + width: ft.Width, + height: ft.Height + } + } else if (shapeType == "Line") { + // {"type":"Line","x1":226.2109090909091,"x2":256,"y1":102.4,"y2":331.40363636363634,"strokeWidth":1,"strokeColor":"#FFFFFF","id":-9286789994829994},{"type":"Arrow","x1":302.54545454545456,"x2":227.1418181818182,"y1":105.19272727272727,"y2":280.20363636363635,"strokeWidth":1,"strokeColor":"#FFFFFF","id":-3936136447472953}], + if (ft.MarkerEnd == "Arrow") { + shapeType = "Arrow" } + shapeJson = { + x1: ft.X1, + y1: ft.Y1, + x2: ft.X2, + y2: ft.Y2 + } + } + + if (shapeJson) { + shapeJson.strokeWidth = ft.StrokeWidth ? ft.StrokeWidth.Value: undefined; + shapeJson.strokeColor = colorIntToHex(ft.StrokeColor); + shapeJson.type = shapeType; } else { console.log("Feature not converted!", ft); } - shapeJson.type = shapeType; return shapeJson; } @@ -90,7 +110,10 @@ export function exportViewersAsPanelsJson() { console.log('regions', regions); if (regions) { regions.forEachFeatureInExtent(vpExtent, function(feature){ - shapes.push(featureToFigureShape(feature)); + let shJson = featureToFigureShape(feature); + if (shJson) { + shapes.push(shJson); + } }); console.log("shapes", shapes); } From 88e4fe32105eebb87c611aea5fb47b0d90f34a9d Mon Sep 17 00:00:00 2001 From: William Moore Date: Tue, 19 Mar 2024 12:43:49 +0000 Subject: [PATCH 05/13] Also support Polyline --- src/utils/figure.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/figure.js b/src/utils/figure.js index ad32e078b..fd2802ba4 100644 --- a/src/utils/figure.js +++ b/src/utils/figure.js @@ -40,7 +40,7 @@ function featureToFigureShape(feature) { radiusY: ft.RadiusY, rotation: 0 } - } else if (shapeType == "Polygon") { + } else if (shapeType == "Polygon" || shapeType == "Polyline") { // "type":"Polygon","points":"188.0795898437498,182.61894531249982 188.0795898437498,182.61894531249982 186.3558 shapeJson = { points: ft.Points From f5a45e055da264fa0a385a5c0d51a6374708e5d2 Mon Sep 17 00:00:00 2001 From: William Moore Date: Wed, 20 Mar 2024 17:58:04 +0000 Subject: [PATCH 06/13] Include panel rotation --- src/utils/figure.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/utils/figure.js b/src/utils/figure.js index fd2802ba4..fe2d4735c 100644 --- a/src/utils/figure.js +++ b/src/utils/figure.js @@ -91,6 +91,11 @@ export function exportViewersAsPanelsJson() { let image_config = viewModel.image_config; let image_info = image_config.image_info; let params = viewModel.viewer.getViewParameters(); + let rotationDegrees = params.rotation * 180 / Math.PI; + if (rotationDegrees < 0) { + rotationDegrees += 360; + } + rotationDegrees = rotationDegrees % 360; // Figure "100%" zoom means image fits in viewport // viewer_ is the OlMap. @@ -105,9 +110,7 @@ export function exportViewersAsPanelsJson() { // Find visible shapes in viewport var shapes = []; let vpExtent = viewModel.viewer.viewer_.getView().calculateExtent(); - console.log("vpExtent", vpExtent); var regions = viewModel.viewer.getRegions(); - console.log('regions', regions); if (regions) { regions.forEachFeatureInExtent(vpExtent, function(feature){ let shJson = featureToFigureShape(feature); @@ -115,7 +118,6 @@ export function exportViewersAsPanelsJson() { shapes.push(shJson); } }); - console.log("shapes", shapes); } // dx and dy will be 0 if centre hasn't moved @@ -158,7 +160,7 @@ export function exportViewersAsPanelsJson() { zoom: panelZoom, dx: dx, dy: dy, - rotation: 0, + rotation: rotationDegrees, shapes, } panels.push(panel); From 08931b1c9ce8358501da1f2d3d923b0770c942b4 Mon Sep 17 00:00:00 2001 From: William Moore Date: Fri, 22 Mar 2024 11:09:55 +0000 Subject: [PATCH 07/13] Fix target tab for opening new Figure files --- src/app/header.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/header.js b/src/app/header.js index d2e10a20b..f71aeb6f7 100644 --- a/src/app/header.js +++ b/src/app/header.js @@ -557,7 +557,7 @@ export class Header { .done(function( data ) { // let fileId = +data; let html = `Figure created: ID "${data}.
    - Open in new tab.`; + Open in new tab.`; Ui.showModalMessage(html, "OK"); }); From 9f22ba44a88528bde6fa49c5bab1736ee00e9061 Mon Sep 17 00:00:00 2001 From: William Moore Date: Fri, 22 Mar 2024 11:45:33 +0000 Subject: [PATCH 08/13] Fix export of page_sizes to figure --- src/app/header.js | 13 ++++--------- src/utils/figure.js | 19 ++++++++++++++++++- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/app/header.js b/src/app/header.js index f71aeb6f7..e928d3939 100644 --- a/src/app/header.js +++ b/src/app/header.js @@ -23,7 +23,7 @@ import JSZip from '../../node_modules/jszip/dist/jszip'; import * as TextEncoding from "../../node_modules/text-encoding"; import Misc from '../utils/misc'; import Ui from '../utils/ui'; -import { exportViewersAsPanelsJson } from '../utils/figure'; +import { exportViewersAsFigureJson } from '../utils/figure'; import {IMAGE_VIEWPORT_CAPTURE} from '../events/events'; import { APP_TITLE, CSV_LINE_BREAK, INITIAL_TYPES, IVIEWER, PROJECTION, @@ -539,21 +539,16 @@ export class Header { return; } - let panels = exportViewersAsPanelsJson(); - const figureName = prompt("Enter Figure name"); if (!figureName) { return; } - let figureJSON = JSON.stringify({ - version: 7, - figureName: figureName, - panels: panels, - }); + let figureJSON = exportViewersAsFigureJson(figureName); + let figureJSONstr = JSON.stringify(figureJSON); // Save - $.post(figureUrl + "/save_web_figure/", {figureJSON,}) + $.post(figureUrl + "/save_web_figure/", {figureJSON: figureJSONstr}) .done(function( data ) { // let fileId = +data; let html = `Figure created: ID "${data}.
    diff --git a/src/utils/figure.js b/src/utils/figure.js index fe2d4735c..a5d00f646 100644 --- a/src/utils/figure.js +++ b/src/utils/figure.js @@ -18,6 +18,9 @@ import {featureToJsonObject} from '../viewers/viewer/utils/Conversion'; +const A4_WIDTH = 595; +const A4_HEIGHT = 842; + function colorIntToHex(signed_integer) { if (typeof signed_integer !== 'number') return null; if (signed_integer < 0) signed_integer = signed_integer >>> 0; @@ -170,7 +173,7 @@ export function exportViewersAsPanelsJson() { let minX = panels.reduce((prev, p) => Math.min(prev, p.x), Infinity); let minY = panels.reduce((prev, p) => Math.min(prev, p.y), Infinity); let maxX = panels.reduce((prev, p) => Math.max(prev, p.x + p.width), 0); - let figureA4width = 595; + let figureA4width = A4_WIDTH; let figureMargin = 20; let availWidth = figureA4width - (2 * figureMargin); let scale = availWidth / (maxX - minX); @@ -184,3 +187,17 @@ export function exportViewersAsPanelsJson() { return panels; } + +export function exportViewersAsFigureJson(figureName) { + + let panels = exportViewersAsPanelsJson() + let figureJson = { + version: 7, + figureName: figureName, + panels, + page_size: "A4", + paper_width: A4_WIDTH, + paper_height: A4_HEIGHT, + } + return figureJson; +} From 2f42c380923f689a2ab6e987157ef82b51524db1 Mon Sep 17 00:00:00 2001 From: William Moore Date: Thu, 28 Mar 2024 17:05:31 +0000 Subject: [PATCH 09/13] Add support for units -> OMERO.figure --- plugin/omero_iviewer/views.py | 10 +++++++--- src/utils/figure.js | 12 +++++++----- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/plugin/omero_iviewer/views.py b/plugin/omero_iviewer/views.py index b7ce0b5ec..510e9c869 100644 --- a/plugin/omero_iviewer/views.py +++ b/plugin/omero_iviewer/views.py @@ -508,18 +508,22 @@ def image_data(request, image_id, conn=None, **kwargs): value = format_pixel_size_with_units(size) rv['pixel_size']['unit_x'] = value[0] rv['pixel_size']['symbol_x'] = value[1] + # id e.g. 'MICROMETER' is used for export to OMERO.figure + rv['pixel_size']['unit_id_x'] = value[2] py = image.getPrimaryPixels().getPhysicalSizeY() if (py is not None): size = image.getPixelSizeY(True) value = format_pixel_size_with_units(size) rv['pixel_size']['unit_y'] = value[0] rv['pixel_size']['symbol_y'] = value[1] + rv['pixel_size']['unit_id_y'] = value[2] pz = image.getPrimaryPixels().getPhysicalSizeZ() if (pz is not None): size = image.getPixelSizeZ(True) value = format_pixel_size_with_units(size) rv['pixel_size']['unit_z'] = value[0] rv['pixel_size']['symbol_z'] = value[1] + rv['pixel_size']['unit_id_z'] = value[2] delta_t_unit_symbol = None rv['delta_t_unit_symbol'] = delta_t_unit_symbol @@ -602,11 +606,11 @@ def format_pixel_size_with_units(value): length = value.getValue() unit = str(value.getUnit()) if unit == "MICROMETER": - unit = lengthunit(length) + symbol = lengthunit(length) length = lengthformat(length) else: - unit = value.getSymbol() - return (length, unit) + symbol = value.getSymbol() + return (length, symbol, unit) @login_required() diff --git a/src/utils/figure.js b/src/utils/figure.js index a5d00f646..8caf713e7 100644 --- a/src/utils/figure.js +++ b/src/utils/figure.js @@ -139,6 +139,7 @@ export function exportViewersAsPanelsJson() { } }); } + pix_size = image_info.image_pixels_size let panel = { x: parseInt(image_config.position.left), @@ -154,11 +155,12 @@ export function exportViewersAsPanelsJson() { orig_height: image_info.dimensions.max_y, sizeZ: image_info.dimensions.max_z, sizeT: image_info.dimensions.max_t, - // TODO: check use of image_info.image_pixels_size.unit_x and .symbol_x etc - // with images of different units - pixel_size_x: image_info.image_pixels_size.x, - pixel_size_y: image_info.image_pixels_size.y, - pixel_size_z: image_info.image_pixels_size.z, + pixel_size_x: pix_size.unit_x, // e.g. 5.0 + pixel_size_y: pix_size.unit_y, + pixel_size_z: pix_size.unit_z, + pixel_size_x_unit: pix_size.unit_id_x, // e.g. "MILLIMETER" + pixel_size_y_unit: pix_size.unit_id_y, + pixel_size_z_unit: pix_size.unit_id_z, deltaT: image_info.image_delta_t, zoom: panelZoom, dx: dx, From 105a9369cdb853508da5ec4e0839c6f448bf3407 Mon Sep 17 00:00:00 2001 From: William Moore Date: Thu, 28 Mar 2024 17:27:56 +0000 Subject: [PATCH 10/13] Handle datasetName to figure --- src/utils/figure.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/utils/figure.js b/src/utils/figure.js index 8caf713e7..86c3363ee 100644 --- a/src/utils/figure.js +++ b/src/utils/figure.js @@ -139,7 +139,8 @@ export function exportViewersAsPanelsJson() { } }); } - pix_size = image_info.image_pixels_size + + const pix_size = image_info.image_pixels_size let panel = { x: parseInt(image_config.position.left), @@ -168,6 +169,10 @@ export function exportViewersAsPanelsJson() { rotation: rotationDegrees, shapes, } + if (image_info.dataset_name && image_info.dataset_name != "Multiple") { + panel.datasetName = image_info.dataset_name; + panel.datasetId = image_info.parent_id; + } panels.push(panel); }); From efda99710532f9743896717e88f0e68ca23ec3b2 Mon Sep 17 00:00:00 2001 From: William Moore Date: Mon, 8 Apr 2024 09:12:32 +0100 Subject: [PATCH 11/13] Fix double-quotes in figure created dialog --- src/app/header.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/header.js b/src/app/header.js index e928d3939..a1826904f 100644 --- a/src/app/header.js +++ b/src/app/header.js @@ -546,12 +546,13 @@ export class Header { let figureJSON = exportViewersAsFigureJson(figureName); let figureJSONstr = JSON.stringify(figureJSON); + console.log('figureJSONstr', figureJSONstr); // Save $.post(figureUrl + "/save_web_figure/", {figureJSON: figureJSONstr}) .done(function( data ) { // let fileId = +data; - let html = `Figure created: ID "${data}.
    + let html = `Figure created: ID ${data}.
    Open in new tab.`; Ui.showModalMessage(html, "OK"); From 409c3429dd72d05fef895dcca6d133f8340eabf4 Mon Sep 17 00:00:00 2001 From: William Moore Date: Mon, 15 Apr 2024 17:56:34 +0100 Subject: [PATCH 12/13] Export Points as Ellipses in OMERO.figure --- src/utils/figure.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/utils/figure.js b/src/utils/figure.js index 86c3363ee..e24caf42a 100644 --- a/src/utils/figure.js +++ b/src/utils/figure.js @@ -67,6 +67,16 @@ function featureToFigureShape(feature) { x2: ft.X2, y2: ft.Y2 } + } else if (shapeType == "Point") { + // 'Point' isn't supported by Figure, but we can use an Ellipse to appear the same + shapeType = "Ellipse"; + shapeJson = { + x: ft.X, + y: ft.Y, + radiusX: 5, + radiusY: 5, + rotation: 0 + } } if (shapeJson) { From 63dd9266a853ff522780066673d0ae2b770238cd Mon Sep 17 00:00:00 2001 From: William Moore Date: Wed, 15 May 2024 14:56:55 +0100 Subject: [PATCH 13/13] Export Points to OMERO.figure as Points (not Ellipse) --- src/app/header.js | 1 - src/utils/figure.js | 7 ++----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/app/header.js b/src/app/header.js index a1826904f..d48765c7f 100644 --- a/src/app/header.js +++ b/src/app/header.js @@ -546,7 +546,6 @@ export class Header { let figureJSON = exportViewersAsFigureJson(figureName); let figureJSONstr = JSON.stringify(figureJSON); - console.log('figureJSONstr', figureJSONstr); // Save $.post(figureUrl + "/save_web_figure/", {figureJSON: figureJSONstr}) diff --git a/src/utils/figure.js b/src/utils/figure.js index e24caf42a..0b9af39e9 100644 --- a/src/utils/figure.js +++ b/src/utils/figure.js @@ -69,13 +69,10 @@ function featureToFigureShape(feature) { } } else if (shapeType == "Point") { // 'Point' isn't supported by Figure, but we can use an Ellipse to appear the same - shapeType = "Ellipse"; + shapeType = "Point"; shapeJson = { x: ft.X, - y: ft.Y, - radiusX: 5, - radiusY: 5, - rotation: 0 + y: ft.Y } }