diff --git a/omero_figure/views.py b/omero_figure/views.py
index f1ca56f0f..04dd9d2e8 100644
--- a/omero_figure/views.py
+++ b/omero_figure/views.py
@@ -124,6 +124,7 @@ def index(request, file_id=None, conn=None, **kwargs):
'const BASE_OMEROWEB_URL = "%s";' % omeroweb_index)
html = html.replace('const APP_ROOT_URL = "";',
'const APP_ROOT_URL = "%s";' % figure_index)
+ # Replace various other placeholder values with OMERO data/configs
html = html.replace('const USER_ID = 0;', 'const USER_ID = %s' % user.id)
html = html.replace('const PING_URL = "";',
'const PING_URL = "%s";' % ping_url)
@@ -403,9 +404,13 @@ def save_web_figure(request, conn=None, **kwargs):
try:
json_data = json.loads(figure_json)
for panel in json_data['panels']:
- image_ids.append(panel['imageId'])
+ try:
+ image_ids.append(int(panel['imageId']))
+ except ValueError:
+ # For NGFF images, the imageId is a string
+ pass
if len(image_ids) > 0:
- first_img_id = int(image_ids[0])
+ first_img_id = image_ids[0]
# remove duplicates
image_ids = list(set(image_ids))
# pretty-print json
@@ -442,7 +447,7 @@ def save_web_figure(request, conn=None, **kwargs):
if file_id is None:
# Create new file
# Try to set Group context to the same as first image
- curr_gid = conn.SERVICE_OPTS.getOmeroGroup()
+ curr_gid = conn.getEventContext().groupId
i = None
if first_img_id:
i = conn.getObject("Image", first_img_id)
diff --git a/package-lock.json b/package-lock.json
index 4752b289b..2c759add0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17,10 +17,12 @@
"jquery": "^3.6.0",
"marked": "^4.2.12",
"mousetrap": "^1.6.5",
+ "ome-zarr.js": "^0.0.17",
"raphael": "^2.3.0",
"sortablejs": "^1.15.2",
"underscore": "^1.13.8",
- "vite-plugin-html-inject": "^1.1.2"
+ "vite-plugin-html-inject": "^1.1.2",
+ "zarrita": "^0.5.4"
},
"devDependencies": {
"sass": "^1.54.1",
@@ -826,6 +828,16 @@
"license": "MIT",
"optional": true
},
+ "node_modules/@zarrita/storage": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/@zarrita/storage/-/storage-0.1.3.tgz",
+ "integrity": "sha512-ZyCMYN3LuCNtKxro9876r/KyHyXV+ie2Bhk1qYsJR4Jp+sAjoVRRNNSJPsJxk64ZgFFezayO5S2hCu88/1Odwg==",
+ "license": "MIT",
+ "dependencies": {
+ "reference-spec-reader": "^0.2.0",
+ "unzipit": "^1.4.3"
+ }
+ },
"node_modules/anymatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
@@ -987,6 +999,12 @@
"resolved": "https://registry.npmjs.org/eve-raphael/-/eve-raphael-0.5.0.tgz",
"integrity": "sha512-jrxnPsCGqng1UZuEp9DecX/AuSyAszATSjf4oEcRxvfxa1Oux4KkIPKBAAWWnpdwfARtr+Q0o9aPYWjsROD7ug=="
},
+ "node_modules/fflate": {
+ "version": "0.8.2",
+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
+ "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
+ "license": "MIT"
+ },
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@@ -1132,6 +1150,24 @@
"node": ">=0.10.0"
}
},
+ "node_modules/numcodecs": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/numcodecs/-/numcodecs-0.3.2.tgz",
+ "integrity": "sha512-6YSPnmZgg0P87jnNhi3s+FVLOcIn3y+1CTIgUulA3IdASzK9fJM87sUFkpyA+be9GibGRaST2wCgkD+6U+fWKw==",
+ "license": "MIT",
+ "dependencies": {
+ "fflate": "^0.8.0"
+ }
+ },
+ "node_modules/ome-zarr.js": {
+ "version": "0.0.17",
+ "resolved": "https://registry.npmjs.org/ome-zarr.js/-/ome-zarr.js-0.0.17.tgz",
+ "integrity": "sha512-5t10obZtLX1Lwgo7kpqGdkPYJOAmRmTojCjhEiOlyDJ66zyyj7PCFN5CkUWsCQSvpWomZ9JwLsjEhv9rhJS4fQ==",
+ "license": "BSD",
+ "dependencies": {
+ "zarrita": "^0.5.0"
+ }
+ },
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -1200,6 +1236,12 @@
"node": ">=8.10.0"
}
},
+ "node_modules/reference-spec-reader": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/reference-spec-reader/-/reference-spec-reader-0.2.0.tgz",
+ "integrity": "sha512-q0mfCi5yZSSHXpCyxjgQeaORq3tvDsxDyzaadA/5+AbAUwRyRuuTh0aRQuE/vAOt/qzzxidJ5iDeu1cLHaNBlQ==",
+ "license": "MIT"
+ },
"node_modules/rollup": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
@@ -1342,6 +1384,24 @@
"integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==",
"license": "MIT"
},
+ "node_modules/unzipit": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/unzipit/-/unzipit-1.4.3.tgz",
+ "integrity": "sha512-gsq2PdJIWWGhx5kcdWStvNWit9FVdTewm4SEG7gFskWs+XCVaULt9+BwuoBtJiRE8eo3L1IPAOrbByNLtLtIlg==",
+ "license": "MIT",
+ "dependencies": {
+ "uzip-module": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/uzip-module": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/uzip-module/-/uzip-module-1.0.3.tgz",
+ "integrity": "sha512-AMqwWZaknLM77G+VPYNZLEruMGWGzyigPK3/Whg99B3S6vGHuqsyl5ZrOv1UUF3paGK1U6PM0cnayioaryg/fA==",
+ "license": "MIT"
+ },
"node_modules/vite": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
@@ -1467,6 +1527,16 @@
"bin": {
"watch": "cli.js"
}
+ },
+ "node_modules/zarrita": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/zarrita/-/zarrita-0.5.4.tgz",
+ "integrity": "sha512-i88iN2+HqIQ+uiCEWLfhjbYNXAJD7IrM4h3lFwFclfqEOOhxp10amRWtqmgN5jbuy3+h0LwdyLVVzk4y9rTLgg==",
+ "license": "MIT",
+ "dependencies": {
+ "@zarrita/storage": "^0.1.3",
+ "numcodecs": "^0.3.2"
+ }
}
}
}
diff --git a/package.json b/package.json
index 81560a36d..96784287d 100644
--- a/package.json
+++ b/package.json
@@ -39,9 +39,11 @@
"jquery": "^3.6.0",
"marked": "^4.2.12",
"mousetrap": "^1.6.5",
+ "ome-zarr.js": "^0.0.17",
"raphael": "^2.3.0",
"sortablejs": "^1.15.2",
"underscore": "^1.13.8",
- "vite-plugin-html-inject": "^1.1.2"
+ "vite-plugin-html-inject": "^1.1.2",
+ "zarrita": "^0.5.4"
}
}
diff --git a/src/js/models/figure_model.js b/src/js/models/figure_model.js
index bf564e30c..8f030a754 100644
--- a/src/js/models/figure_model.js
+++ b/src/js/models/figure_model.js
@@ -7,9 +7,10 @@
import { recoverFigureFromStorage,
clearFigureFromStorage,
figureConfirmDialog,
- getJson,
+ getJsonWithCredentials,
saveFigureToStorage,
normalizeZProjectionBounds} from "../views/util";
+ import { loadZarrForPanel } from "./zarr_utils";
// Version of the json file we're saving.
// This only needs to increment when we make breaking changes (not linked to release versions.)
@@ -66,7 +67,7 @@
var load_url = BASE_WEBFIGURE_URL + "load_web_figure/" + fileId + "/",
self = this;
- getJson(load_url).then(data => {
+ getJsonWithCredentials(load_url).then(data => {
data.fileId = fileId;
self.load_from_JSON(data);
self.set('unsaved', false);
@@ -310,7 +311,7 @@
if (iids.length > 0) {
var ptUrl = BASE_WEBFIGURE_URL + 'pixels_type/';
ptUrl += '?image=' + iids.join('&image=');
- getJson(ptUrl).then(data => {
+ getJsonWithCredentials(ptUrl).then(data => {
// Update all panels
// NB: By the time that this callback runs, the panels will have been created
self.panels.forEach(function(p){
@@ -533,6 +534,11 @@
// new image panels appropriately in a grid.
var invalidIds = [];
for (var i=0; i${fmt} not currently supported.
+ Please
+ open in the OME-NGFF Validator to choose a single Image url.
+ `;
+ }
+ }
+ if (zarrErr.includes("File not found")) {
+ zarrErr = `Error loading Zarr from
${zarrUrl}:
+ File not found (No zarr.json or .zattrs)
`;
+ }
+ figureConfirmDialog(
+ "Zarr Load Error",
+ zarrErr,
+ ["OK"]
+ );
+ // alert(`Error loading Zarr ${zarrUrl}: ${panel_json.Error}`);
+ this.set('loading_count', this.get('loading_count') - 1);
+ return;
+ }
+
+ // coords (px, py etc) are incremented for each panel added
+ this.updateCoordsAndPanelCoords(panel_json, coords, index)
+
+ this.set('loading_count', this.get('loading_count') - 1);
+ // create Panel (and select it)
+ // We do some additional processing in Panel.parse()
+ this.panels.create(panel_json, {'parse': true}).set('selected', true);
+ this.notifySelectionChange();
+ },
+
importImage: function(imgDataUrl, coords, baseUrl, index) {
var self = this,
@@ -580,23 +653,6 @@
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;
-
- // calculate panel coordinates from index...
- var row = parseInt(index / coords.colCount, 10);
- var col = index % coords.colCount;
- var panelX = coords.px + ((data.size.width + coords.spacer) * coords.scale * col);
- var panelY = coords.py + ((data.size.height + coords.spacer) * coords.scale * row);
-
// ****** This is the Data Model ******
//-------------------------------------
// Any changes here will create a new version
@@ -617,8 +673,8 @@
'channels': data.channels,
'orig_width': data.size.width,
'orig_height': data.size.height,
- 'x': panelX,
- 'y': panelY,
+ 'x': 0,
+ 'y': 0,
'datasetName': data.meta.datasetName,
'datasetId': data.meta.datasetId,
'pixel_size_x': data.pixel_size.valueX,
@@ -636,6 +692,10 @@
if (baseUrl) {
n.baseUrl = baseUrl;
}
+
+ // coords (px, py etc) are incremented for each panel added
+ self.updateCoordsAndPanelCoords(n, coords, index);
+
// create Panel (and select it)
// We do some additional processing in Panel.parse()
self.panels.create(n, {'parse': true}).set('selected', true);
diff --git a/src/js/models/panel_model.js b/src/js/models/panel_model.js
index ba4d626e7..7efd6ed2d 100644
--- a/src/js/models/panel_model.js
+++ b/src/js/models/panel_model.js
@@ -3,6 +3,7 @@
import _ from "underscore";
import $ from "jquery";
import { rotatePoint, figureConfirmDialog, normalizeZProjectionBounds } from "../views/util";
+ import {renderZarrToSrc} from "./zarr_utils";
// Corresponds to css - allows us to calculate size of labels
var LINE_HEIGHT = 1.43;
@@ -1032,7 +1033,33 @@
return this.get('orig_width') * this.get('orig_height') > MAX_PLANE_SIZE;
},
- get_img_src: function(force_no_padding) {
+ get_zarr_img_src: async function(force_no_padding, targetSize) {
+ var rect;
+ if (this.is_big_image()) {
+ rect = this.getViewportAsRect();
+ if (!force_no_padding) {
+ var length = Math.max(rect.width, rect.height) * 1.5;
+ rect.x = rect.x - ((length - rect.width) / 2);
+ rect.y = rect.y - ((length - rect.height) / 2);
+ rect.width = length;
+ rect.height = length;
+ }
+ } else {
+ // Since we render the whole plane every time (no 'rect' here), caching will prevent reloading on pan/zoom.
+ // as long as targetSize doesn't change. So we can afford to render at higher resolution...
+ targetSize = Math.max(targetSize, 2000);
+ }
+ return renderZarrToSrc(this.get('imageId'), this.get('zarr'), this.get('theZ'), this.get('theT'), this.get('channels'), rect, targetSize);
+ },
+
+ get_img_src: async function(force_no_padding) {
+ // async function since zarr src is the rendered image data
+ if (this.get("zarr")) {
+ // use current size to choose resolution level...
+ let targetSize = 4 * Math.max(this.get('width'), this.get('height'));
+ targetSize = Math.max(targetSize, 500);
+ return this.get_zarr_img_src(force_no_padding, targetSize);
+ }
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;
@@ -1500,7 +1527,11 @@
createLabelsFromTags: function(options) {
// Loads Tags for selected images and creates labels
- var image_ids = this.map(function(s){return s.get('imageId')})
+ var image_ids = this.map(function(s){return s.get('imageId')});
+ image_ids = _.uniq(image_ids).filter(id => !isNaN(id)); // ignore zarr images
+ if (image_ids.length === 0) {
+ return;
+ }
image_ids = "image=" + image_ids.join("&image=");
// TODO: Use /api/ when annotations is supported
var url = WEBINDEX_URL + "api/annotations/?type=tag&parents=true&limit=1000&" + image_ids;
diff --git a/src/js/models/zarr_utils.js b/src/js/models/zarr_utils.js
new file mode 100644
index 000000000..520bf910c
--- /dev/null
+++ b/src/js/models/zarr_utils.js
@@ -0,0 +1,483 @@
+import * as zarr from "zarrita";
+import * as omezarr from "ome-zarr.js";
+import { slice } from "zarrita";
+import _ from 'underscore';
+import { getJson, FILE_NOT_FOUND } from "../views/util.js";
+
+const ZARRITA_ARRAY_CACHE = {};
+const ZARR_DATA_CACHE = {};
+
+export async function loadZarrForPanel(zarrUrl) {
+
+ // first check if we have a zarr image...
+ let zarrJson;
+ try {
+ zarrJson = await getJson(zarrUrl + "/zarr.json");
+ zarrJson = zarrJson.attributes?.ome; // zarr v3
+ } catch (error) {
+ console.log("Error loading zarr.json:", error);
+ if (error.toString().includes(FILE_NOT_FOUND)) {
+ try {
+ zarrJson = await getJson(zarrUrl + "/.zattrs");
+ } catch (error2) {
+ return {"Error": error2.toString()};
+ }
+ } else {
+ return {"Error": error.toString()};
+ }
+ }
+ if (!zarrJson) {
+ return {"Error": "Failed to load Zarr metadata"};
+ }
+ if (zarrJson["bioformats2raw.layout"]) {
+ return {"Error": "bioformats2raw.layout is not currently supported"};
+ }
+ if (zarrJson.plate) {
+ return {"Error": "OME-Zarr Plates are not currently supported"};
+ }
+
+ let store = new zarr.FetchStore(zarrUrl);
+
+ // we load smallest array. Only need it for min/max values if not in 'omero' metadata
+ let datasetIndex = -1;
+ let msWithArray;
+ try {
+ msWithArray = await omezarr.getMultiscaleWithArray(store, datasetIndex);
+ } catch (error) {
+ console.error("Error loading Zarr:", error);
+ return {"Error": error.toString()};
+ }
+ console.log("msWithArray", msWithArray);
+ const {arr, multiscale, omero, scales, zarr_version} = msWithArray;
+ let zarrName = zarrUrl.split("/").pop();
+ console.log("multiscale", multiscale);
+ if (!multiscale) {
+ return {"Error": `Failed to load multiscale`};
+ }
+
+ let imgName = multiscale.name || zarrName;
+ let axes = multiscale.axes;
+ let axesNames = axes?.map((axis) => axis.name) || ["t", "c", "z", "y", "x"];
+
+ let datasets = multiscale.datasets;
+ let zarrays = {};
+ // 'consolidate' the metadata for all arrays
+ for (let ds of datasets) {
+ let path = ds.path;
+ let ds_array = await omezarr.getArray(store, path, zarr_version);
+ zarrays[path] = {shape: ds_array.shape, dtype: ds_array.dtype};
+ }
+ // store under 'arrays' key
+ let zarr_attrs = {
+ multiscales: [multiscale],
+ }
+ zarr_attrs["arrays"] = zarrays;
+ zarr_attrs["zarr_version"] = zarr_version;
+
+ if (multiscale.version) {
+ zarr_attrs["version"] = multiscale.version;
+ } else if (zarr_version == "3") {
+ zarr_attrs["version"] = zarrJson.version;
+ }
+
+ let zarray = zarrays[datasets[0].path];
+ console.log("zarray", zarray);
+
+ let dtype = zarray.dtype;
+ let shape = zarray.shape;
+ let dims = shape.length;
+ let sizeX = shape[dims - 1];
+ let sizeY = shape[dims - 2];
+ let sizeZ = 1;
+ let sizeT = 1;
+ let sizeC = 1;
+ if (axesNames.includes("z")) {
+ sizeZ = shape[axesNames.indexOf("z")];
+ }
+ let defaultZ = parseInt(sizeZ / 2);
+ if (axesNames.includes("t")) {
+ sizeT = shape[axesNames.indexOf("t")];
+ }
+ let defaultT = parseInt(sizeT / 2);
+ if (axesNames.includes("c")) {
+ sizeC = shape[axesNames.indexOf("c")];
+ }
+
+ // channels...
+ // if no omero data, need to construct channels!
+ let default_colors = [
+ "FF0000",
+ "00FF00",
+ "0000FF",
+ "FFFF00",
+ "00FFFF",
+ "FFFFFF",
+ ];
+ let chs = omero?.channels;
+ let indices = {};
+ if (axesNames.includes("z")) {
+ indices["z"] = defaultZ;
+ }
+ // placeholder minmax values
+ let minMaxs = _.range(sizeC).map((idx) => [0, 255]);
+ let minMaxProvided = chs && chs.every((ch) => ch.window && ch.window.min != undefined && ch.window.max != undefined);
+ if (!minMaxProvided) {
+ // load smallest array to get min/max values for every channel
+ let slices = omezarr.getSlices(_.range(sizeC), arr.shape, axesNames, indices, shape);
+ let promises = slices.map((chSlice) => zarr.get(arr, chSlice));
+ let ndChunks = await Promise.all(promises);
+
+ minMaxs = _.range(sizeC).map((idx) => {
+ return omezarr.getMinMaxValues(ndChunks[idx]);
+ });
+ }
+
+ // use channel metadata if provided, otherwise default values or values from smallest array
+ let channels = _.range(sizeC).map((idx) => {
+ let ch = chs ? chs[idx] : null;
+ let mm = [ch?.window?.min ?? minMaxs[idx][0], ch?.window?.max ?? minMaxs[idx][1]];
+
+ return {
+ label: ch?.label || "Ch" + idx,
+ active: ch?.active !== undefined ? ch.active : true,
+ color: ch?.color || default_colors[idx % default_colors.length],
+ window: {
+ min: mm[0],
+ max: mm[1],
+ start: ch?.window?.start ?? mm[0],
+ end: ch?.window?.end ?? mm[1],
+ },
+ };
+ });
+
+ let deltaT = [];
+ if (axesNames.includes("t")) {
+ // if we have time units...
+ let timeAxis = axes?.find((a) => a.name == "t");
+ if (timeAxis && timeAxis.unit) {
+ let secsIncrement = 1;
+ let scaleTransform0 = multiscale.datasets[0].coordinateTransformations?.find(
+ (ct) => ct.type == "scale"
+ );
+ if (scaleTransform0) {
+ secsIncrement = scaleTransform0.scale[axesNames.indexOf("t")];
+ }
+ const LENGTH_UNITS = {
+ "nanosecond": 1e-9,
+ "microsecond": 1e-6,
+ "millisecond": 1e-3,
+ "second": 1,
+ "minute": 60,
+ "hour": 3600,
+ };
+ if (LENGTH_UNITS[timeAxis.unit]) {
+ secsIncrement = secsIncrement * LENGTH_UNITS[timeAxis.unit];
+ for(let t = 0; t < sizeT; t++) {
+ deltaT.push(t * secsIncrement);
+ }
+ }
+ }
+ }
+
+ let panelX = 0;
+ let panelY = 0;
+ let coords = {scale: 1};
+
+ // ****** 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: zarrUrl,
+ name: imgName,
+ width: sizeX * coords.scale,
+ height: sizeY * coords.scale,
+ sizeZ: sizeZ,
+ theZ: defaultZ,
+ sizeT: sizeT,
+ theT: defaultT,
+ rdefs: { model: "-" },
+ channels: channels,
+ orig_width: sizeX,
+ orig_height: sizeY,
+ x: panelX,
+ y: panelY,
+ // 'deltaT': data.deltaT,
+ pixelsType: dtypeToPixelsType(dtype),
+ // 'pixel_range': data.pixel_range,
+ // let's dump the zarr data into the panel
+ zarr: zarr_attrs,
+ };
+
+ if (deltaT.length > 0) {
+ n['deltaT'] = deltaT;
+ }
+
+ // handle pixel sizes if available
+ // Use 'scale' from first 'coordinateTransforms' if available
+ let datasetScale = multiscale.datasets[0].coordinateTransformations?.find(
+ (ct) => ct.type == "scale"
+ );
+ let msScale = multiscale.coordinateTransformations?.find(
+ (ct) => ct.type == "scale"
+ );
+ if (datasetScale) {
+ for (let dimName of ["x", "y", "z"]) {
+ axes.forEach((axis, idx) => {
+ if (axis.name == dimName) {
+ let dimScale = datasetScale.scale[idx];
+ if (msScale) {
+ // also apply any 'scale' on multiscale.coordinateTransformations
+ dimScale = dimScale * msScale.scale[idx];
+ }
+ // One OF: angstrom, attometer, centimeter, decimeter, exameter, femtometer, foot, gigameter, hectometer, inch, kilometer, megameter, meter, micrometer, mile, millimeter, nanometer, parsec, petameter, picometer, terameter, yard, yoctometer, yottameter, zeptometer, zettameter
+ if (axis.unit) {
+ let unitKey = axis.unit.toUpperCase();
+ n['pixel_size_' + dimName] = dimScale;
+ n['pixel_size_' + dimName + '_unit'] = unitKey;
+ if (LENGTH_UNITS[unitKey]) {
+ n['pixel_size_' + dimName + '_symbol'] = LENGTH_UNITS[unitKey].symbol;
+ }
+ }
+ }
+ });
+ }
+ }
+
+ console.log("Zarr Panel Model...", n);
+
+ return n;
+}
+
+// e.g. " "uint8"
+function dtypeToPixelsType(dtype) {
+ let dt = "";
+ if (dtype.includes("u")) {
+ dt += "uint";
+ } else if (dtype.includes("i")) {
+ dt += "int";
+ } else if (dtype.includes("f")) {
+ dt += "float";
+ }
+ if (dtype.includes("8")) {
+ dt += "64";
+ } else if (dtype.includes("4")) {
+ dt += "32";
+ } else if (dtype.includes("2")) {
+ dt += "16";
+ } else if (dtype.includes("1")) {
+ dt += "8";
+ }
+ return dt;
+};
+
+export async function renderZarrToSrc(source, attrs, theZ, theT, channels, rect, targetSize=500) {
+ let paths = attrs.multiscales[0].datasets.map((d) => d.path);
+ // for v0.3 each axes is a string, for v0.4+ it is an object
+ let axes = attrs.multiscales[0].axes?.map((a) => a.name || a);
+ // If no axes (v0.1, v0.2) it must be 5D
+ axes = axes || ["t", "c", "z", "y", "x"];
+ let zarrays = attrs.arrays;
+
+ // Pick resolution where crop size is closest to targetSize
+ // use the arrays themselves to determine 'scale', since we might
+ // not have 'coordinateTransforms' for pre-v0.4 etc.
+ let path;
+
+ // size of full-size image
+ let fullShape = zarrays[paths[0]].shape;
+
+ let region_x = rect?.x || 0;
+ let region_y = rect?.y || 0;
+ let region_width = rect?.width || fullShape.at(-1);
+ let region_height = rect?.height || fullShape.at(-2);
+ let region_max_size = Math.max(region_width, region_height);
+
+ let dsScales = paths.map((p) => zarrays[p].shape.at(-1) / fullShape.at(-1));
+ // E.g. if dataset shape is 1/2 of fullShape then crop size will be half
+ let cropSizes = dsScales.map((s) => region_max_size * s);
+
+ // find the closest matching size...
+ let targetScale;
+ for (let i = 0; i < cropSizes.length; i++) {
+ // if we've gone small enough, or at last one...
+ if (cropSizes[i] <= targetSize || i == cropSizes.length - 1) {
+ if (Math.abs(cropSizes[i] - targetSize) < Math.abs(cropSizes[Math.max(0, i - 1)] - targetSize)) {
+ path = paths[i];
+ targetScale = dsScales[i];
+ } else {
+ path = paths[Math.max(0, i - 1)];
+ targetScale = dsScales[Math.max(0, i - 1)];
+ }
+ break;
+ }
+ }
+ let array_rect = {
+ x: Math.floor(region_x * targetScale),
+ y: Math.floor(region_y * targetScale),
+ width: Math.floor(region_width * targetScale),
+ height: Math.floor(region_height * targetScale),
+ };
+
+ // Handle any Z-downsampling...
+ let sizeZ = zarrays[paths[0]].shape[axes.indexOf("z")];
+ let cropZ = zarrays[path].shape[axes.indexOf("z")];
+ theZ = Math.floor(theZ *cropZ / sizeZ);
+
+ // We can create canvas of the size of the array_rect
+ const canvas = document.createElement("canvas");
+ canvas.width = array_rect.width;
+ canvas.height = array_rect.height;
+
+ if (!path) {
+ console.error(
+ `Lowest resolution too large for rendering: > ${targetSize} x ${targetSize}`
+ );
+ return;
+ }
+
+ let storeArrayPath = source + "/" + path;
+ let arr;
+ if (ZARRITA_ARRAY_CACHE[storeArrayPath]) {
+ arr = ZARRITA_ARRAY_CACHE[storeArrayPath];
+ } else {
+ let store = new zarr.FetchStore(source + "/" + path);
+ arr = await zarr.open(store, { kind: "array" });
+ ZARRITA_ARRAY_CACHE[storeArrayPath] = arr;
+ }
+
+ let chDim = axes.indexOf("c");
+ let shape = zarrays[path].shape;
+ let dims = shape.length;
+
+ let activeChIndicies = [];
+ let colors = [];
+ let minMaxValues = [];
+ let luts = [];
+ let inverteds = [];
+ channels.forEach((ch, index) => {
+ if (ch.active) {
+ activeChIndicies.push(index);
+ colors.push(hexToRGB(ch.color));
+ minMaxValues.push([ch.window.start, ch.window.end]);
+ luts.push(ch.color.endsWith(".lut") ? ch.color : undefined);
+ inverteds.push(ch.reverseIntensity);
+ }
+ });
+
+ // Need same logic as https://github.com/ome/omero-figure/blob/9cc36cde05bde4def7b62b07c2e9ae5f66712e96/omero_figure/views.py#L310
+ // if we have a crop region outside the bounds of the image
+ let array_shape = zarrays[path].shape;
+ let size_x = array_shape.at(-1);
+ let size_y = array_shape.at(-2);
+ let paste_x = 0;
+ let paste_y = 0;
+ // let {x, y, width, height} = array_rect;
+ if (array_rect.x < 0) {
+ paste_x = - array_rect.x
+ array_rect.width += array_rect.x;
+ array_rect.x = 0;
+ }
+ if (array_rect.y < 0) {
+ paste_y = - array_rect.y;
+ array_rect.height += array_rect.y;
+ array_rect.y = 0;
+ }
+ if (array_rect.x + array_rect.width > size_x) {
+ array_rect.width = size_x - array_rect.x;
+ }
+ if (array_rect.y + array_rect.height > size_y) {
+ array_rect.height = size_y - array_rect.y;
+ }
+
+ let promises = activeChIndicies.map((chIndex) => {
+ let sliceKey = [];
+ let slices = shape.map((dimSize, index) => {
+ // channel
+ if (index == chDim) {
+ sliceKey.push("" + chIndex);
+ return chIndex;
+ }
+ // x and y - crop to rect
+ if (axes[index] == "x") {
+ sliceKey.push(`${array_rect.x}:${array_rect.x + array_rect.width}`);
+ return slice(array_rect.x, array_rect.x + array_rect.width);
+ }
+ if (axes[index] == "y") {
+ sliceKey.push(`${array_rect.y}:${array_rect.y + array_rect.height}`);
+ return slice(array_rect.y, array_rect.y + array_rect.height);
+ }
+ // z: TODO: handle case where lower resolution is downsampled in Z
+ if (axes[index] == "z") {
+ sliceKey.push("" + theZ);
+ return theZ;
+ }
+ if (axes[index] == "t") {
+ sliceKey.push("" + theT);
+ return theT;
+ }
+ return 0;
+ });
+ let cacheKey = `${source}/${path}/${sliceKey.join(",")}`;
+ // If we have requested slice in cache, return that instead of loading chunks...
+ if (ZARR_DATA_CACHE[cacheKey]) {
+ if (ZARR_DATA_CACHE[cacheKey] !== "pending") {
+ console.log("RETURN cache!", ZARR_DATA_CACHE[cacheKey]);
+ return ZARR_DATA_CACHE[cacheKey];
+ } else {
+ // data is pending...
+ console.log("PENDING cache...", cacheKey);
+ // wait until data is populated, check every 100ms
+ return new Promise((resolve, reject) => {
+ let checkInterval = setInterval(() => {
+ if (ZARR_DATA_CACHE[cacheKey] && ZARR_DATA_CACHE[cacheKey] !== "pending") {
+ console.log("RESOLVE pending cache!", ZARR_DATA_CACHE[cacheKey]);
+ clearInterval(checkInterval);
+ resolve(ZARR_DATA_CACHE[cacheKey]);
+ }
+ }, 100);
+ });
+ }
+ }
+
+ // "pending" flag to avoid duplicate loads
+ ZARR_DATA_CACHE[cacheKey] = "pending";
+ return zarr.get(arr, slices).then((data) => {
+ console.log("populate cache...");
+ ZARR_DATA_CACHE[cacheKey] = data;
+ return data;
+ });
+ });
+
+ let ndChunks = await Promise.all(promises);
+ let start = new Date().getTime();
+ let rbgData = omezarr.renderTo8bitArray(
+ ndChunks,
+ minMaxValues,
+ colors,
+ luts,
+ inverteds
+ );
+ console.log("renderTo8bitArray took", new Date().getTime() - start, "ms");
+
+ let chunk_width = ndChunks[0].shape.at(-1);
+ let chunk_height = ndChunks[0].shape.at(-2);
+
+ const context = canvas.getContext("2d");
+ context.fillStyle = "#ddd";
+ context.fillRect(0, 0, canvas.width, canvas.height);
+ context.putImageData(new ImageData(rbgData, chunk_width, chunk_height), paste_x, paste_y);
+ let dataUrl = canvas.toDataURL("image/png");
+ return dataUrl;
+}
+
+export function hexToRGB(hex) {
+ if (hex.startsWith("#")) hex = hex.slice(1);
+ const r = parseInt(hex.slice(0, 2), 16);
+ const g = parseInt(hex.slice(2, 4), 16);
+ const b = parseInt(hex.slice(4, 6), 16);
+ return [r, g, b];
+}
diff --git a/src/js/views/channel_slider_view.js b/src/js/views/channel_slider_view.js
index 505cc6701..a5253b9b6 100644
--- a/src/js/views/channel_slider_view.js
+++ b/src/js/views/channel_slider_view.js
@@ -137,6 +137,9 @@ var ChannelSliderView = Backbone.View.extend({
return false;
}
var c = this.hexToRgb(color);
+ if (!c) {
+ return false;
+ }
var min, max, delta;
var v, s, h;
min = Math.min(c.r, c.g, c.b);
diff --git a/src/js/views/chgrp_modal_view.js b/src/js/views/chgrp_modal_view.js
index eab664664..984a16a86 100644
--- a/src/js/views/chgrp_modal_view.js
+++ b/src/js/views/chgrp_modal_view.js
@@ -5,7 +5,11 @@ import _ from "underscore";
import FigureModel from "../models/figure_model";
-import {figureConfirmDialog, hideModal, getJson} from "./util";
+import {
+ figureConfirmDialog,
+ hideModal,
+ getJsonWithCredentials
+} from "./util";
export const ChgrpModalView = Backbone.View.extend({
@@ -41,7 +45,7 @@ export const ChgrpModalView = Backbone.View.extend({
loadImageDetails: function() {
var imgIds = this.model.panels.pluck('imageId');
var url = `${BASE_WEBFIGURE_URL}images_details/?image=${_.uniq(imgIds).join(',')}`;
- getJson(url).then(data => {
+ getJsonWithCredentials(url).then(data => {
// Sort images by Group
this.imagesByGroup = data.data.reduce(function(prev, img){
if (!prev[img.group.id]) {
@@ -56,7 +60,7 @@ export const ChgrpModalView = Backbone.View.extend({
loadGroups: function() {
var url = `${API_BASE_URL_V0}m/experimenters/${USER_ID}/experimentergroups/`;
- getJson(url).then(data => {
+ getJsonWithCredentials(url).then(data => {
this.omeroGroups = data.data.map(group => {return {id: group['@id'], name: group.Name}})
.filter(group => group.name != 'user');
this.render();
diff --git a/src/js/views/crop_modal_view.js b/src/js/views/crop_modal_view.js
index 47e4c26be..d6d3e27bb 100644
--- a/src/js/views/crop_modal_view.js
+++ b/src/js/views/crop_modal_view.js
@@ -6,7 +6,7 @@ import Raphael from "raphael";
import FigureModel from "../models/figure_model";
import RectView from "./raphael-rect";
-import {figureConfirmDialog, hideModal, getJson, rotatePoint} from "./util";
+import {figureConfirmDialog, hideModal, getJsonWithCredentials, rotatePoint} from "./util";
import crop_modal_roi_template from '../../templates/modal_dialogs/crop_modal_roi.template.html?raw';
@@ -399,7 +399,7 @@ export const CropModalView = Backbone.View.extend({
iid = self.m.get('imageId');
var offset = this.roisPageSize * this.roisPage;
var url = BASE_WEBFIGURE_URL + 'roiRectangles/' + iid + '/?limit=' + self.roisPageSize + '&offset=' + offset;
- getJson(url).then(rsp => {
+ getJsonWithCredentials(url).then(rsp => {
var data = rsp.data;
self.roisLoaded += data.length;
self.roisPage += 1;
@@ -513,7 +513,6 @@ export const CropModalView = Backbone.View.extend({
let rotation = rect.rotation || 0;
if (rect.theT > -1) this.m.set('theT', rect.theT, {'silent': true});
if (rect.theZ > -1) this.m.set('theZ', rect.theZ, {'silent': true});
- src = this.m.get_img_src(true);
if (rect.width > rect.height) {
div_w = size;
div_h = (rect.height/rect.width) * div_w;
@@ -529,8 +528,12 @@ export const CropModalView = Backbone.View.extend({
rect.theT = rect.theT !== undefined ? rect.theT : origT;
rect.theZ = rect.theZ !== undefined ? rect.theZ : origZ;
let css = this.m._viewport_css(left, top, img_w, img_h, size, size, rotation);
+ let random_id = "rect_" + Math.random();
+ this.m.get_img_src(true)
+ .then(src => document.getElementById(random_id).src = src);
var json = {
+ 'id': random_id,
'msg': msg,
'src': src,
'rect': rect,
@@ -586,7 +589,8 @@ export const CropModalView = Backbone.View.extend({
this.m.set('zoom', 100);
this.m.set('width', newW);
this.m.set('height', newH);
- var src = this.m.get_img_src(true);
+ this.m.get_img_src(true)
+ .then(src => this.$cropImg.attr('src', src));
var rotation = this.m.get('rotation') || 0;
// Calculate bounding box for rotated image
diff --git a/src/js/views/info_panel_view.js b/src/js/views/info_panel_view.js
index ab5ab9e90..4b9c66387 100644
--- a/src/js/views/info_panel_view.js
+++ b/src/js/views/info_panel_view.js
@@ -164,22 +164,24 @@ var InfoPanelView = Backbone.View.extend({
this.$el.append(this.xywh_template(json));
},
- getImageLinks: function(remoteUrl, imageIds, imageNames) {
+ getImageLinks: function(imageIds, imageNames) {
// Link if we have a single remote image, E.g. http://jcb-dataviewer.rupress.org/jcb/img_detail/625679/
+ var selectedObjs = imageIds.map((id, index) => {return {id, name: imageNames[index], type: 'image'}})
+ .filter(function(idWithName){
+ return !isNaN(idWithName.id);
+ });
+ var zarrIds = imageIds.filter(function(id){
+ return typeof id === "string" && id.indexOf('zarr') > -1;
+ });
var imageLinks = [];
- if (remoteUrl) {
- if (imageIds.length == 1) {
- imageLinks.push({'text': 'Image viewer', 'url': remoteUrl});
- }
- // OR all the images are local...
- } else {
- imageLinks.push({'text': 'Webclient', 'url': WEBINDEX_URL + "?show=image-" + imageIds.join('|image-')});
+
+ // Handle Image ID links to OMERO...
+ if (selectedObjs.length > 0) {
+ var numberIds = selectedObjs.map(idWithName => idWithName.id);
+ imageLinks.push({'text': 'Webclient', 'url': WEBINDEX_URL + "?show=image-" + numberIds.join('|image-')});
// Handle other 'Open With' options
OPEN_WITH.forEach(function(v){
- var selectedObjs = imageIds.map(function(id, i){
- return {'id': id, 'name': imageNames[i], 'type': 'image'};
- });
var enabled = false;
if (typeof v.isEnabled === "function") {
enabled = v.isEnabled(selectedObjs);
@@ -204,6 +206,10 @@ var InfoPanelView = Backbone.View.extend({
imageLinks.push({'text': label, 'url': url});
});
}
+ // Handle Zarr links to ome-ngff-validator
+ if (zarrIds.length == 1) {
+ imageLinks.push({'text': "ome-ngff-validator", 'url': "https://ome.github.io/ome-ngff-validator/?source=" + zarrIds[0]});
+ }
return imageLinks;
},
@@ -216,16 +222,11 @@ var InfoPanelView = Backbone.View.extend({
// Flag to ignore blur events caused by $el.html() below
this.rendering = true;
var json,
- title = this.models.length + " Panels Selected...",
- remoteUrl;
+ title = this.models.length + " Panels Selected...";
var imageIds = this.models.pluck('imageId');
var imageNames = this.models.pluck('name');
this.models.forEach(function(m) {
- if (m.get('baseUrl')) {
- // only used when a single image is selected
- remoteUrl = m.get('baseUrl') + "/img_detail/" + m.get('imageId') + "/";
- }
// start with json data from first Panel
var this_json = m.toJSON();
// Format floating point values
@@ -274,7 +275,7 @@ var InfoPanelView = Backbone.View.extend({
json.export_dpi = Math.max(json.export_dpi, json.min_export_dpi);
}
- json.imageLinks = this.getImageLinks(remoteUrl, imageIds, imageNames);
+ json.imageLinks = this.getImageLinks(imageIds, imageNames);
// all setId if we have a single Id
json.setImageId = _.uniq(imageIds).length == 1;
diff --git a/src/js/views/labels_from_maps_modal.js b/src/js/views/labels_from_maps_modal.js
index 74b9252e9..857dc4bad 100644
--- a/src/js/views/labels_from_maps_modal.js
+++ b/src/js/views/labels_from_maps_modal.js
@@ -4,7 +4,7 @@ import $ from "jquery";
import _ from "underscore";
import FigureModel from "../models/figure_model";
-import { getJson, hideModal } from "./util";
+import { getJsonWithCredentials, hideModal } from "./util";
export var LabelFromMapsModal = Backbone.View.extend({
@@ -39,13 +39,19 @@ export var LabelFromMapsModal = Backbone.View.extend({
*/
loadMapAnns() {
let imageIds = this.model.getSelected().map(function(m){return m.get('imageId')});
+ imageIds = _.uniq(imageIds).filter(id => !isNaN(id)); // ignore zarr images
+ if (imageIds.length === 0) {
+ this.annotations = [];
+ this.render();
+ return;
+ }
this.isLoading = true;
$('select', this.el).html("");
var url = WEBINDEX_URL + "api/annotations/?type=map&parents=true&image=";
url += imageIds.join("&image=");
- getJson(url).then(data => {
+ getJsonWithCredentials(url).then(data => {
if (data.hasOwnProperty('parents')){
data.parents.annotations.forEach(function(ann) {
let class_ = ann.link.parent.class;
diff --git a/src/js/views/lutpicker.js b/src/js/views/lutpicker.js
index 471fa730e..304eb137e 100644
--- a/src/js/views/lutpicker.js
+++ b/src/js/views/lutpicker.js
@@ -23,7 +23,7 @@ import _ from 'underscore';
import * as bootstrap from "bootstrap"
import lut_picker_template from '../../templates/lut_picker.template.html?raw';
-import { showModal, getJson } from "./util";
+import { showModal } from "./util";
// Need to handle dev vv built (omero-web) paths
import lutsPng from "../../images/luts_10.png";
diff --git a/src/js/views/modal_views.js b/src/js/views/modal_views.js
index a5b8b630f..ae760ca68 100644
--- a/src/js/views/modal_views.js
+++ b/src/js/views/modal_views.js
@@ -522,42 +522,13 @@ import { hideModal } from "./util";
if (!idInput || idInput.length === 0) return;
// test for E.g: http://localhost:8000/webclient/?show=image-25|image-26|image-27
- if (idInput.indexOf('?') > 10) {
+ if (idInput.indexOf('?show') > 10) {
iIds = idInput.split('image-').slice(1);
- } else if (idInput.indexOf('img_detail') > 0) {
- // url of image viewer...
- this.importFromRemote(idInput);
- return;
} else {
+ // iIds could be Zarr URLs - NB: this will fail if a Zarr URL contains ','
iIds = idInput.split(',');
}
this.model.addImages(iIds);
},
-
- importFromRemote: function(img_detail_url) {
- var iid = parseInt(img_detail_url.split('img_detail/')[1], 10),
- baseUrl = img_detail_url.split('/img_detail')[0],
- // http://jcb-dataviewer.rupress.org/jcb/imgData/25069/
- imgDataUrl = baseUrl + '/imgData/' + iid + "/";
-
- var colCount = 1,
- rowCount = 1,
- paper_width = this.model.get('paper_width'),
- c = this.figureView.getCentre(),
- col = 0,
- row = 0,
- px, py, spacer, scale,
- coords = {'px': px,
- 'py': py,
- 'c': c,
- 'spacer': spacer,
- 'colCount': colCount,
- 'rowCount': rowCount,
- 'col': col,
- 'row': row,
- 'paper_width': paper_width};
-
- this.model.importImage(imgDataUrl, coords, baseUrl);
- },
});
diff --git a/src/js/views/panel_view.js b/src/js/views/panel_view.js
index 93d475e49..8886f8282 100644
--- a/src/js/views/panel_view.js
+++ b/src/js/views/panel_view.js
@@ -194,15 +194,18 @@
// But we don't want the previous image showing while we wait...
if (this.model.is_big_image()) {
this.$img_panel.hide();
- $(".image_panel_spinner", this.$el).show();
}
+ let timeoutId = setTimeout(() => {
+ $(".image_panel_spinner", this.$el).show();
+ }, 100); // Show spinner only if image load takes longer than 100ms
this.$img_panel.one("load", function(){
+ clearTimeout(timeoutId);
$(".image_panel_spinner", this.$el).hide();
this.$img_panel.show();
}.bind(this));
- var src = this.model.get_img_src();
- this.$img_panel.attr('src', src);
+ this.model.get_img_src()
+ .then(src => this.$img_panel.attr('src', src));
// if a 'reasonable' dpi is set, we don't pixelate
if (this.model.get('min_export_dpi') > 100) {
diff --git a/src/js/views/right_panel_view.js b/src/js/views/right_panel_view.js
index 9b1282cf3..38e4c3f0a 100644
--- a/src/js/views/right_panel_view.js
+++ b/src/js/views/right_panel_view.js
@@ -1173,9 +1173,16 @@
var frame_h = this.full_size / wh;
}
this.models.forEach(function(m){
- var src = m.get_img_src();
+ var img_id = "image_" + Math.random();
+ // src is async, so we set an id instead. When src returns we
+ // use the ID to find the element and set the src.
+ m.get_img_src().then(src => {
+ document.getElementById(img_id).src = src;
+ });
var img_css = m.get_vp_img_css(m.get('zoom'), frame_w, frame_h);
- img_css.src = src;
+ // placeholder transparent gif
+ img_css.src = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
+ img_css.id = img_id;
// if a 'reasonable' dpi is set, we don't pixelate
var dpiSet = m.get('min_export_dpi') > 100;
img_css.pixelated = !dpiSet;
diff --git a/src/js/views/roi_modal_view.js b/src/js/views/roi_modal_view.js
index 1ead692eb..b66ac4d93 100644
--- a/src/js/views/roi_modal_view.js
+++ b/src/js/views/roi_modal_view.js
@@ -13,7 +13,7 @@ import shape_toolbar_template from '../../templates/shapes/shape_toolbar.templat
import shape_sidebar_template from '../../templates/shapes/shape_sidebar.template.html?raw';
import roi_zt_buttons from '../../templates/modal_dialogs/roi_zt_buttons.template.html?raw';
import RoiLoaderView from './roi_loader_view';
-import { hideModal, getJson } from "./util";
+import { hideModal, getJsonWithCredentials } from "./util";
const TEXT_ANCHOR_ICONS = {
"start": "bi-text-left",
@@ -182,7 +182,7 @@ export const RoiModalView = Backbone.View.extend({
.attr({'disabled': 'disabled'});
$btn.parent().attr('title', 'Checking for ROIs...'); // title on parent div - still works if btn disabled
- getJson(url).then((data) => {
+ getJsonWithCredentials(url).then((data) => {
this.omeroRoiCount = data.roi;
this.renderSidebar();
});
@@ -222,7 +222,7 @@ export const RoiModalView = Backbone.View.extend({
var iid = this.m.get('imageId');
let url = BASE_OMEROWEB_URL + 'api/v0/m/rois/';
var roiUrl = url + '?image=' + iid + '&limit=' + this.roisPageSize + '&offset=' + (this.roisPageSize * this.roisPage);
- getJson(roiUrl).then((data) => {
+ getJsonWithCredentials(roiUrl).then((data) => {
this.Rois.set(data.data);
$("#loadRois").prop('disabled', false);
$("#roiModalRoiList table").empty();
@@ -551,48 +551,9 @@ export const RoiModalView = Backbone.View.extend({
$("#roiModalSidebar", this.$el).html(this.sidebar_template(json));
},
- // for rendering bounding-box viewports for shapes
- getBboxJson: function(bbox, theZ, theT) {
- var size = 50; // longest side
- 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');
- // theT = (theT !== undefined ? theT : this.m.get('theT'))
- var div_w, div_h;
- // get src for image by temp setting Z & T
- if (theT !== undefined) this.m.set('theT', bbox.theT, {'silent': true});
- if (theZ !== undefined) this.m.set('theZ', bbox.theZ, {'silent': true});
- var src = this.m.get_img_src();
- if (bbox.width > bbox.height) {
- div_w = size;
- div_h = (bbox.height/bbox.width) * div_w;
- } else {
- div_h = size;
- div_w = (bbox.width/bbox.height) * div_h;
- }
- var zoom = div_w/bbox.width;
- var img_w = orig_width * zoom;
- var img_h = orig_height * zoom;
- var top = -(zoom * bbox.y);
- var left = -(zoom * bbox.x);
- // bbox.theT = bbox.theT !== undefined ? bbox.theT : origT;
- // bbox.theZ = bbox.theZ !== undefined ? bbox.theZ : origZ;
-
- return {
- 'src': src,
- 'w': div_w,
- 'h': div_h,
- 'top': top,
- 'left': left,
- 'img_w': img_w,
- 'img_h': img_h
- };
- },
-
renderImagePlane: function() {
- var src = this.m.get_img_src();
- this.$roiImg.attr('src', src);
+ this.m.get_img_src()
+ .then(src => this.$roiImg.attr('src', src));
var orig_model = this.model.getSelected().head();
var json = {'theZ': this.m.get('theZ'),
diff --git a/src/js/views/util.js b/src/js/views/util.js
index 4551b4793..05af24088 100644
--- a/src/js/views/util.js
+++ b/src/js/views/util.js
@@ -256,11 +256,57 @@ export function hideModal(modalId) {
thisModal.hide();
}
-export async function getJson (url) {
+export async function getJsonWithCredentials (url) {
+ // $.getJSON() also works for OMERO.web requests
let cors_headers = { mode: 'cors', credentials: 'include' };
return fetch(url, cors_headers).then(rsp => rsp.json());
}
+export const FILE_NOT_FOUND = "File not found";
+export async function getJson(url) {
+ // This will throw an Error if 404 or CORS fails etc
+ return fetchHandleError(url).then(rsp => rsp.json());
+}
+
+async function fetchHandleError(url) {
+ let msg = `Error Loading ${url}:`;
+ let rsp;
+ try {
+ rsp = await fetch(url).then(function (response) {
+ if (!response.ok) {
+ // make the promise be rejected if we didn't get a 2xx response
+ // NB. statusText could be "Not Found" or "File not found" depending on server
+ // Standardise based on response.status
+ if (response.status == 404) {
+ msg += ` ${FILE_NOT_FOUND}`;
+ } else {
+ msg += ` ${response.statusText}`;
+ }
+ } else {
+ return response;
+ }
+ });
+ } catch (error) {
+ console.log("check for CORS...");
+ console.log(error);
+ try {
+ let corsRsp = await fetch(url, { mode: "no-cors" });
+ console.log("corsRsp", corsRsp);
+ // If the 'no-cors' mode allows this to return, then we
+ // likely failed due to CORS in the original request
+ msg += " Failed due to CORS issues.";
+ } catch (anotherError) {
+ console.log("Even `no-cors` request failed!", anotherError);
+ // return the original error (same as anotherError?)
+ msg += ` ${error}`;
+ }
+ }
+ if (rsp) {
+ return rsp;
+ }
+ throw Error(msg);
+}
+
export const RANDOM_NUMBER_RANGE = 100000000;
export function getRandomId() {
diff --git a/src/templates/figure_panel.template.html b/src/templates/figure_panel.template.html
index 65a808962..087acdcde 100644
--- a/src/templates/figure_panel.template.html
+++ b/src/templates/figure_panel.template.html
@@ -1,6 +1,6 @@
-
+
diff --git a/src/templates/info_panel.template.html b/src/templates/info_panel.template.html
index 7d97cb8b0..5154d290b 100644
--- a/src/templates/info_panel.template.html
+++ b/src/templates/info_panel.template.html
@@ -14,6 +14,12 @@
<% }) %>
|
+ <% if (isNaN(imageId)) { %>
+ URL:
+
+ <%= imageId %>
+
+ <% } else { %>
Image ID:
<%= imageId %>
@@ -24,6 +30,7 @@
Edit ID
+ <% } %>
Dimensions (XY):
<%= orig_width %> x <%= orig_height %>
diff --git a/src/templates/modal_dialogs/crop_modal_roi.template.html b/src/templates/modal_dialogs/crop_modal_roi.template.html
index 53ecab399..1901cb504 100644
--- a/src/templates/modal_dialogs/crop_modal_roi.template.html
+++ b/src/templates/modal_dialogs/crop_modal_roi.template.html
@@ -2,7 +2,8 @@
|
- ![]()
- ![]()
+ import * as zarr from "https://cdn.jsdelivr.net/npm/zarrita@next/+esm";
+ const store = new zarr.FetchStore("https://raw.githubusercontent.com/zarr-developers/zarr_implementations/5dc998ac72/examples/zarr.zr/blosc");
+ const arr = await zarr.open(store, { kind: "array" });
+ console.log(arr);
+ // {
+ // store: FetchStore,
+ // path: "/",
+ // dtype: "uint8",
+ // shape: [512, 512, 3],
+ // chunks: [100, 100, 1],
+ // }
+ const view = await zarr.get(arr, [null, null, 0]);
+ // {
+ // data: Uint8Array,
+ // shape: [512, 512],
+ // stride: [512, 1],
+ // }
+ console.log(view);
+
+
+
+ Test
+
\ No newline at end of file
|