diff --git a/docs/figure_file_format.rst b/docs/figure_file_format.rst
index 1bd45fb74..77cac2c44 100644
--- a/docs/figure_file_format.rst
+++ b/docs/figure_file_format.rst
@@ -36,7 +36,7 @@ This is an example of a minimal OMERO.figure JSON file::
{
// see older versions below
- "version": 5,
+ "version": 6,
"panels": [
{
// position of the panel on the page
@@ -82,6 +82,11 @@ Optional settings for each panel::
// options are omero.model.LengthI.SYMBOLS.keys()
"pixel_size_x_unit": 'MICROMETER',
+ // pixel size z to show z position in labels
+ "pixel_size_z": 0.32,
+ "pixel_size_z_symbol": 'µm', // µm by default
+ "pixel_size_z_unit": 'MICROMETER',
+
// show a scalebar
"scalebar": {
"show": true,
@@ -108,12 +113,22 @@ Optional settings for each panel::
"color": "00FF00"
},
{
- // 'live' timestamps, 'time' one of: index (show 1-based T index), milliseconds,
+ // Dynamic properties: text in labels in the form '[property.format]'
+ // are dynamically replaced by the specified values
+
+ // for 'time' property, 'format' one of: index (show 1-based T index), milliseconds,
// secs, mins:secs, mins, hrs:mins, hrs:mins:secs,
- "time": "milliseconds",
+ "text": "[time.milliseconds]",
"size": "12",
"position": "topleft",
"color": "FFFFFF"
+ },
+ {
+ // for 'x' and 'y' property, 'format' one of: pixel, unit
+ "text": "X: [x.pixel] - Y: [y.pixel]",
+ "size": "12",
+ "position": "topright",
+ "color": "FFFFFF"
}
],
@@ -137,6 +152,8 @@ Optional settings for each panel::
// panel rotation in degrees clockwise
rotation: 0,
+ // rotation symbol to display in label
+ rotation_symbol:'°',
Optional settings for the top-level figure object. If not specified,
@@ -208,6 +225,11 @@ that lines will not appear thicker on a panel when it is zoomed in. Supported Sh
Version history
----------------
+New in version 6:
+
+- 'label': 'time':'seconds' changed to 'text':'[time.seconds]' (for all timestamp formats)
+- 'panel': z pixel properties added ('pixel_size_z', 'pixel_size_z_symbol', 'pixel_size_z_unit')
+
New in version 5:
- `scalebar`: added 'pixel_size_x_unit': "MICROMETER".
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 0e50d2a7d..53b422ef3 100644
--- a/omero_figure/scripts/omero/figure_scripts/Figure_To_Pdf.py
+++ b/omero_figure/scripts/omero/figure_scripts/Figure_To_Pdf.py
@@ -28,6 +28,7 @@
import zipfile
from math import atan2, atan, sin, cos, sqrt, radians, floor, ceil
from copy import deepcopy
+import re
from omero.model import ImageAnnotationLinkI, ImageI, LengthI
import omero.scripts as scripts
@@ -1092,23 +1093,27 @@ def get_time_label_text(self, delta_t, format):
is_negative = delta_t < 0
delta_t = abs(delta_t)
text = "%d s" % int(round(delta_t))
- if format == "milliseconds":
+ if format in ["milliseconds", "ms"]:
text = "%s ms" % int(round(delta_t * 1000))
- elif format == "mins":
+ elif format in ["secs", "seconds", "s"]:
+ text = "%d s" % int(round(delta_t))
+ elif format in ["mins", "minutes", "m"]:
text = "%s mins" % int(round(delta_t / 60))
- elif format == "mins:secs":
+ elif format in ["mins:secs", "m:s"]:
m = int(delta_t // 60)
s = round(delta_t % 60)
text = "%s:%02d" % (m, s)
- elif format == "hrs:mins":
+ elif format in ["hrs:mins", "h:m"]:
h = int(delta_t // 3600)
m = int(round((delta_t % 3600) / 60))
text = "%s:%02d" % (h, m)
- elif format == "hrs:mins:secs":
+ elif format in ["hrs:mins:secs", "h:m:s"]:
h = int(delta_t // 3600)
m = (delta_t % 3600) // 60
s = round(delta_t % 60)
text = "%s:%02d:%02d" % (h, m, s)
+ else: # Format unknown
+ return ""
if text in ["0 s", "0:00", "0 mins", "0:00:00"]:
is_negative = False
return ('-' if is_negative else '') + text
@@ -1136,6 +1141,8 @@ def draw_labels(self, panel, page):
width = panel['width']
height = panel['height']
+ viewport_region = self.get_crop_region(panel)
+
# Handle page offsets
x = x - page['x']
y = y - page['y']
@@ -1148,18 +1155,122 @@ def draw_labels(self, panel, page):
'topleft': [], 'topright': [],
'bottomleft': [], 'bottomright': []}
+ parse_re = re.compile(r"\[.+?\]")
for l in labels:
- if 'text' not in l:
- the_t = panel['theT']
- timestamps = panel.get('deltaT')
- if l.get('time') == "index":
- l['text'] = str(the_t + 1)
- elif timestamps and panel['theT'] < len(timestamps):
- d_t = panel['deltaT'][the_t]
- l['text'] = self.get_time_label_text(d_t, l['time'])
- else:
- continue
-
+ # Substitution of special label by their values
+ new_text = []
+ last_idx = 0
+ for item in parse_re.finditer(l['text']):
+ new_text.append(l['text'][last_idx:item.start()])
+ expr = item.group()[1:-1].split(".")
+ label_value = ""
+
+ if expr[0] in ["time", "t"]:
+ the_t = panel['theT']
+ timestamps = panel.get('deltaT')
+ # default to index
+ if len(expr) == 1 or expr[1] == "index":
+ label_value = str(the_t + 1)
+ else:
+ if timestamps and the_t < len(timestamps):
+ d_t = timestamps[the_t]
+ else:
+ d_t = 0
+ label_value = self.get_time_label_text(d_t, expr[1])
+
+ elif expr[0] == "image":
+ format = expr[1] if len(expr) > 1 else "name"
+ if format == "name":
+ label_value = panel['name'].split('/')[-1]
+ elif format == "id":
+ label_value = str(panel['imageId'])
+ # Escaping "_" for markdown
+ label_value = label_value.replace("_", "\\_")
+
+ elif expr[0] == "dataset":
+ format = expr[1] if len(expr) > 1 else "name"
+ if format == "name":
+ if panel['datasetName']:
+ label_value = panel['datasetName']
+ else:
+ label_value = "No/Many Datasets"
+ elif format == "id":
+ if panel['datasetId']:
+ label_value = panel['datasetName']
+ else:
+ label_value = "null"
+ # Escaping "_" for markdown
+ label_value = label_value.replace("_", "\\_")
+
+ elif expr[0] in ['x', 'y', 'z', 'width', 'height',
+ 'w', 'h', 'rotation', 'rot']:
+ format = expr[1] if len(expr) > 1 else "pixel"
+ if format == "px":
+ format = "pixel"
+ prop = expr[0]
+ if prop == "w":
+ prop = "width"
+ elif prop == "h":
+ prop = "height"
+ elif prop == "rot":
+ prop = "rotation"
+
+ if prop == "z":
+ size_z = panel.get('sizeZ')
+ pixel_size_z = panel.get('pixel_size_z')
+ z_symbol = panel.get('pixel_size_z_symbol')
+ if pixel_size_z is None:
+ pixel_size_z = 0
+
+ if ("z_projection" in panel.keys()
+ and panel["z_projection"]):
+ z_start, z_end = panel["z_start"], panel["z_end"]
+ if format == "pixel":
+ label_value = (str(z_start + 1) + "-"
+ + str(z_end + 1))
+ elif format == "unit" and size_z:
+ z_start = f"{(z_start * pixel_size_z):.2f}"
+ z_end = f"{(z_end * pixel_size_z):.2f}"
+ label_value = (z_start + " " + z_symbol + " - "
+ + z_end + " " + z_symbol)
+ else:
+ the_z = panel['theZ'] if panel['theZ'] else 0
+ if format == "pixel":
+ label_value = str(the_z + 1)
+ elif (format == "unit" and size_z
+ and the_z < size_z):
+ z_pos = f"{(the_z * pixel_size_z):.2f}"
+ label_value = (z_pos + " " + z_symbol)
+
+ elif prop == "rotation":
+ label_value = (str(int(panel["rotation"]))
+ + panel['rotation_symbol'])
+
+ else:
+ value = viewport_region[prop]
+ if format == "pixel":
+ label_value = str(int(value))
+ elif format == "unit":
+ if prop in ['x', 'width']:
+ scale = panel['pixel_size_x']
+ elif prop in ['y', 'height']:
+ scale = panel['pixel_size_y']
+ rounded = f"{(value * scale):.2f}"
+ label_value = ("" + rounded +
+ " " + panel['pixel_size_x_symbol'])
+
+ elif expr[0] in ["channels", "c"]:
+ label_value = []
+ for channel in panel["channels"]:
+ if channel["active"]:
+ label_value.append(channel["label"])
+ label_value = " ".join(label_value)
+
+ new_text.append(label_value if label_value else item.group())
+ last_idx += item.end()
+
+ new_text.append(l['text'][last_idx:])
+ l['text'] = "".join(new_text)
pos = l['position']
l['size'] = int(l['size']) # make sure 'size' is number
# If page is black and label is black, make label white
diff --git a/omero_figure/static/figure/css/figure.css b/omero_figure/static/figure/css/figure.css
index eec7f13ba..8bd5b9ec4 100644
--- a/omero_figure/static/figure/css/figure.css
+++ b/omero_figure/static/figure/css/figure.css
@@ -167,16 +167,6 @@
padding: 3px 15px;
}
- .markdown-info {
- padding: 3px 12px;
- color: #aaa;
- text-align: right;
- padding-left: 40px;
- background: url(../images/markdown_light32x20.png) 0% center no-repeat;
- }
- .markdown-info:hover {
- background: url(../images/markdown_dark32x20.png) 0% center no-repeat;
- }
.legend-container .markdown-info {
display: none;
}
@@ -184,12 +174,6 @@
display: block;
}
- #labelsTab .markdown-info {
- height: 18px;
- margin: 9px 9px 0;
- float: left"
- }
-
/* hide appropriate collapse/hide button */
.legend-collapsed .collapse-legend{
display: none;
diff --git a/omero_figure/templates/figure/index.html b/omero_figure/templates/figure/index.html
index 51c2ee3b7..2b8ab2ece 100644
--- a/omero_figure/templates/figure/index.html
+++ b/omero_figure/templates/figure/index.html
@@ -514,9 +514,10 @@
Import from JSON
-
Markdown Formatting
+
Label Formatting
-
+
+
Markdown formatting
You can use "Markdown" syntax to add formatting to plain text. For example,
**this text will be bold** and this *word* will be italic. NB: Links are not
@@ -545,6 +546,52 @@
Markdown Formatting
+
Dynamic properties
+
+ The drop-down menu in the 'Add Labels' form allows you to create labels based on Image metadata.
+ Labels created with square brackets indicate dynamic properties. The properties within the square
+ brackets will be dynamically updated upon changes. Additional text can be added outside the square
+ brackets and will not be affected when the labels are displayed.
+ The following table shows some examples, including the use of dynamic properties and markdown together.
+
+
+
+
+
This text will be formatted to...
+
...this text
+
+
+
X and Y in pixels
+
X:[x.pixel] - Y:[y.pixel]
+
X:52 - Y:89
+
+
+
X and Y in units
+
X:[x.unit] - Y:[y.unit]
+
X:0.43 µm - Y:0.23 µm
+
+
+
Width
+
Width: [width.pixel]
+
Width: 400
+
+
+
Height (with markdown formatting)
+
*Height*: **[height.pixel]**
+
Height: 300
+
+
+
Image ID
+
Image ID: [image.id]
+
Image ID: 23556
+
+
+
Dataset ID
+
Dataset ID: [dataset.id]
+
Dataset ID: 1654
+
+
+
Legend
The figure legend will be included in the PDF info page when the figure is
exported.
@@ -1020,7 +1067,9 @@
Scalebar
Add Labels
+ title="Show label formatting options...">
+ Tips
+
diff --git a/omero_figure/urls.py b/omero_figure/urls.py
index 115075f75..3fe6ea502 100644
--- a/omero_figure/urls.py
+++ b/omero_figure/urls.py
@@ -73,6 +73,10 @@
# Use query ?image=1&image=2
url(r'^timestamps/$', views.timestamps, name='figure_timestamps'),
+ # Get Z scale for images
+ # Use query ?image=1&image=2
+ url(r'^z_scale/$', views.z_scale, name='figure_z_scale'),
+
url(r'^roiCount/(?P[0-9]+)/$', views.roi_count,
name='figure_roiCount'),
diff --git a/omero_figure/views.py b/omero_figure/views.py
index 93c463920..d7b1358a9 100644
--- a/omero_figure/views.py
+++ b/omero_figure/views.py
@@ -130,6 +130,11 @@ def img_data_json(request, image_id, conn=None, **kwargs):
rv['pixel_size']['valueY'] = py.getValue()
rv['pixel_size']['symbolY'] = py.getSymbol()
rv['pixel_size']['unitY'] = str(py.getUnit())
+ pz = image.getPrimaryPixels().getPhysicalSizeZ()
+ if pz is not None:
+ rv['pixel_size']['valueZ'] = pz.getValue()
+ rv['pixel_size']['symbolZ'] = pz.getSymbol()
+ rv['pixel_size']['unitZ'] = str(pz.getUnit())
size_t = image.getSizeT()
time_list = []
if size_t > 1:
@@ -151,6 +156,22 @@ def timestamps(request, conn=None, **kwargs):
return JsonResponse(data)
+@login_required()
+def z_scale(request, conn=None, **kwargs):
+
+ iids = request.GET.getlist('image')
+ data = {}
+ for iid in iids:
+ image = conn.getObject('Image', iid)
+ if image is not None:
+ pz = image.getPrimaryPixels().getPhysicalSizeZ()
+ if pz is not None:
+ data[image.id] = {'valueZ': pz.getValue(),
+ 'symbolZ': pz.getSymbol(),
+ 'unitZ': str(pz.getUnit())}
+ return JsonResponse(data)
+
+
@login_required()
def render_scaled_region(request, iid, z, t, conn=None, **kwargs):
diff --git a/src/js/models/figure_model.js b/src/js/models/figure_model.js
index 4f283716f..6543433b6 100644
--- a/src/js/models/figure_model.js
+++ b/src/js/models/figure_model.js
@@ -1,7 +1,7 @@
// 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 = 5;
+ var VERSION = 6;
// ------------------------- Figure Model -----------------------------------
@@ -221,6 +221,44 @@
});
}
}
+
+ if (v < 6) {
+ console.log("Transforming to VERSION 6");
+ // Adding the Z scale to the model
+ var iids = [];
+ _.each(json.panels, function(p) {
+ if (iids.indexOf(p.imageId) == -1) {
+ iids.push(p.imageId)
+ }
+ });
+ if (iids.length > 0) {
+ zUrl = BASE_WEBFIGURE_URL + 'z_scale/';
+ zUrl += '?image=' + iids.join('&image=');
+ $.getJSON(zUrl, function(data) {
+ // Update all panels
+ // NB: By the time that this callback runs, the panels will have been created
+ self.panels.forEach(function(p){
+ var iid = p.get('imageId');
+ if (data[iid]) {
+ p.set('pixel_size_z', data[iid].valueZ);
+ p.set('pixel_size_z_symbol', data[iid].symbolZ);
+ p.set('pixel_size_z_unit', data[iid].unitZ);
+ }
+ });
+ });
+ }
+
+ // Converting the time-labels to V6 syntax, all other special label were converted to text
+ _.each(json.panels, function(p) {
+ for (var i=0; i
-1) {
isNegative = false;
@@ -308,18 +326,80 @@
return (isNegative ? '-' : '') + text;
},
- create_labels_from_time: function(options) {
+ get_name_label_text: function(property, format) {
+ var text = "";
+ if (property === "image") {
+ if (format === "id") {
+ text = ""+this.get('imageId');
+ } else if (format === "name") {
+ var pathnames = this.get('name').split('/');
+ text = pathnames[pathnames.length-1];
+ }
+ } else if (property === "dataset"){
+ if (format === "id") {
+ text = ""+this.get('datasetId');
+ } else if (format === "name") {
+ text = this.get('datasetName') ? this.get('datasetName') : "No/Many Datasets";
+ }
+ }
+ return text;
+ },
+
+ get_view_label_text: function(property, format) {
+ if (format === "px") format = "pixel";
+
+ if (property === "w") property = "width";
+ else if (property === "h") property = "height";
+ else if (property === "rot") property = "rotation";
+
- this.add_labels([{
- 'time': options.format,
- 'size': options.size,
- 'position': options.position,
- 'color': options.color
- }]);
+ x_symbol = this.get('pixel_size_x_symbol');
+ z_symbol = this.get('pixel_size_z_symbol');
+ z_symbol = z_symbol?z_symbol:x_symbol // Using x symbol when z not defined
+ x_size = this.get('pixel_size_x');
+ y_size = this.get('pixel_size_y');
+ z_size = this.get('pixel_size_z');
+ z_size = z_size?z_size:0
+
+ var text = "";
+ if (property === "z") {
+ if (this.get('z_projection')) {
+ start = this.get('z_start');
+ end = this.get('z_end');
+ if (format === "pixel") {
+ text = "" + (start+1) + " - " + (end+1);
+ } else if (format === "unit"){
+ start = (start * z_size).toFixed(2)
+ end = (end * z_size).toFixed(2)
+ text = ""+ start +" "+ z_symbol
+ + " - " + end +" "+ z_symbol
+ }
+ }
+ else {
+ var theZ = this.get('theZ');
+ if (format === "pixel") {
+ text = "" + (theZ + 1);
+ } else if (format === "unit"){
+ text = ""+ (theZ * z_size).toFixed(2) +" "+ z_symbol
+ }
+ }
+ return text
+ }
+ viewport = this.getViewportAsRect();
+ value = viewport[property];
+ if (property === "rotation") {
+ return ""+parseInt(value)+"°";
+ } else if (format === "pixel") {
+ return ""+parseInt(value);
+ } else if (format === "unit") {
+ scale = ['x', 'width'].includes(property) ? x_size : y_size
+ text = ""+ (value * scale).toFixed(2) +" "+ x_symbol
+ }
+ return text
},
get_label_key: function(label) {
- var key = (label.text || label.time) + '_' + label.size + '_' + label.color + '_' + label.position;
+ var key = label.text + '_' + label.size + '_' + label.color + '_' + label.position;
key = _.escape(key);
return key;
},
diff --git a/src/js/views/panel_view.js b/src/js/views/panel_view.js
index 3ef16864f..7f0e9cdc5 100644
--- a/src/js/views/panel_view.js
+++ b/src/js/views/panel_view.js
@@ -21,7 +21,9 @@
this.listenTo(this.model,
'change:zoom change:dx change:dy change:width change:height change:channels change:theZ change:theT change:z_start change:z_end change:z_projection change:min_export_dpi',
this.render_image);
- this.listenTo(this.model, 'change:labels change:theT change:deltaT', this.render_labels);
+ this.listenTo(this.model,
+ 'change:channels change:zoom change:dx change:dy change:width change:height change:rotation change:labels change:theT change:deltaT change:theZ change:deltaZ change:z_projection change:z_start change:z_end',
+ this.render_labels);
this.listenTo(this.model, 'change:shapes', this.render_shapes);
// During drag or resize, model isn't updated, but we trigger 'drag'
@@ -188,12 +190,37 @@
ljson.color = '000000';
}
}
- if (typeof ljson.text == 'undefined' && ljson.time) {
- ljson.text = self.model.get_time_label_text(ljson.time);
- } else {
- // Markdown also escapes all labels so they are safe
- ljson.text = markdown.toHTML(ljson.text);
+ const matches = [...ljson.text.matchAll(/\[.+?\]/g)]; // Non greedy regex capturing expressions in []
+ if (matches.length>0){
+ var new_text = "";
+ var last_idx = 0;
+ for (const match of matches) {// Loops on the match to replace in the ljson.text the expression by their values
+ var new_text = new_text + ljson.text.slice(last_idx, match.index);
+ expr = match[0].slice(1,-1).split(".");
+ var label_value = ""
+ if (['time', 't'].includes(expr[0])) {
+ label_value = self.model.get_time_label_text(expr[1] ? expr[1] : "index");
+ } else if (['image', 'dataset'].includes(expr[0])){
+ label_value = self.model.get_name_label_text(expr[0], expr[1] ? expr[1] : "name");
+ //Escape the underscore for markdown
+ label_value = label_value.replaceAll("_", "\\_");
+ } else if (['x', 'y', 'z', 'width', 'height', 'w', 'h', 'rotation', 'rot'].includes(expr[0])){
+ label_value = self.model.get_view_label_text(expr[0], expr[1] ? expr[1] : "pixel");
+ } else if (['channels', 'c'].includes(expr[0])) {
+ label_value = self.model.get_channels_label_text();
+ }
+
+ //If label_value hasn't been created (invalid expr[0])
+ // or is empty (invalid expr[1]), the expr is kept unmodified
+ new_text = new_text + (label_value?label_value:match[0]);
+ last_idx = match.index + match[0].length;
+ }
+ ljson.text = new_text + ljson.text.slice(last_idx);
}
+
+ // Markdown also escapes all labels so they are safe
+ ljson.text = markdown.toHTML(ljson.text);
+
positions[l.position].push(ljson);
});
diff --git a/src/js/views/right_panel_view.js b/src/js/views/right_panel_view.js
index 6ddbe1224..c91641e52 100644
--- a/src/js/views/right_panel_view.js
+++ b/src/js/views/right_panel_view.js
@@ -337,25 +337,13 @@
var selected = this.model.getSelected();
- if (label_text == '[channels]') {
+ if (label_text == '[channels labels]') {
selected.forEach(function(m) {
m.create_labels_from_channels({position:position, size:font_size});
});
return false;
}
- if (label_text.slice(0, 5) == '[time') {
- var format = label_text.slice(6, -1); // 'secs', 'hrs:mins' etc
- selected.forEach(function(m) {
- m.create_labels_from_time({format: format,
- position:position,
- size:font_size,
- color: color
- });
- });
- return false;
- }
-
if (label_text == '[key-values]') {
// Load Map Annotations for this image and create labels
$("#labelsFromMapAnns").modal("show", {
@@ -383,12 +371,6 @@
};
selected.forEach(function(m) {
- if (label_text === "[image-name]") {
- var pathnames = m.get('name').split('/');
- label.text = pathnames[pathnames.length-1];
- } else if (label_text === "[dataset-name]") {
- label.text = m.get('datasetName') ? m.get('datasetName') : "No/Many Datasets";
- }
m.add_labels([label]);
});
return false;
@@ -513,12 +495,6 @@
key = _.escape(key);
var new_label = {text:label_text, size:font_size, position:position, color:color};
- // if we're editing a 'time' label, preserve the 'time' attribute
- if (label_text.slice(0, 5) == '[time') {
- new_label.text = undefined; // no 'text'
- new_label.time = label_text.slice(6, -1); // 'secs', 'hrs:mins' etc
- }
-
var newlbls = {};
newlbls[key] = new_label;
@@ -540,10 +516,6 @@
var key = m.get_label_key(l),
ljson = $.extend(true, {}, l);
ljson.key = key;
- if (typeof ljson.text == 'undefined' && ljson.time) {
- // show time labels as they are in 'new label' form
- ljson.text = '[time-' + ljson.time + "]"
- }
positions[l.position][key] = ljson;
});
});
diff --git a/src/templates/labels_form_inner_template.html b/src/templates/labels_form_inner_template.html
index 5fd7dd2ab..efb061fc7 100644
--- a/src/templates/labels_form_inner_template.html
+++ b/src/templates/labels_form_inner_template.html
@@ -11,10 +11,13 @@