From b5f4916bc9e4fb3e1b7d075ebb9b7719e79f67ce Mon Sep 17 00:00:00 2001
From: William Moore <w.moore@dundee.ac.uk>
Date: Wed, 29 Sep 2021 17:40:25 +0100
Subject: [PATCH 1/3] Display collection using Plate layout

---
 src/io.ts  |   6 ++--
 src/ome.ts | 103 ++++++++++++++++++++++++++++++++++++++---------------
 2 files changed, 78 insertions(+), 31 deletions(-)

diff --git a/src/io.ts b/src/io.ts
index 08458f3a..f201642b 100644
--- a/src/io.ts
+++ b/src/io.ts
@@ -1,7 +1,7 @@
 import { DTYPE_VALUES, ImageLayer, MultiscaleImageLayer, ZarrPixelSource } from '@hms-dbmi/viv';
 import { Group as ZarrGroup, openGroup, ZarrArray } from 'zarr';
 import GridLayer from './gridLayer';
-import { loadOmeroMultiscales, loadPlate, loadWell } from './ome';
+import { loadCollection, loadOmeroMultiscales, loadWell } from './ome';
 import type {
   ImageLayerConfig,
   LayerState,
@@ -111,8 +111,8 @@ export async function createSourceData(config: ImageLayerConfig): Promise<Source
   if (node instanceof ZarrGroup) {
     const attrs = (await node.attrs.asObject()) as Ome.Attrs;
 
-    if ('plate' in attrs) {
-      return loadPlate(config, node, attrs.plate);
+    if ('collection' in attrs || 'plate' in attrs) {
+      return loadCollection(config, node, attrs);
     }
 
     if ('well' in attrs) {
diff --git a/src/ome.ts b/src/ome.ts
index d9a4ea48..78cdd79e 100644
--- a/src/ome.ts
+++ b/src/ome.ts
@@ -107,25 +107,78 @@ export async function loadWell(config: ImageLayerConfig, grp: ZarrGroup, wellAtt
   return sourceData;
 }
 
-export async function loadPlate(config: ImageLayerConfig, grp: ZarrGroup, plateAttrs: Ome.Plate): Promise<SourceData> {
-  if (!('columns' in plateAttrs) || !('rows' in plateAttrs)) {
-    throw Error(`Plate .zattrs missing columns or rows`);
+async function getImagePaths(grp: ZarrGroup, omeAttrs: Ome.Attrs): Promise<string[]> {
+  if ('collection' in omeAttrs) {
+    return Object.keys(omeAttrs.collection.images);
+  } else if ('plate' in omeAttrs) {
+    // Load each Well to get a path/to/image/
+    const wellPaths = omeAttrs.plate.wells.map((well) => well.path);
+    async function getImgPath(wellPath: string) {
+      const wellAttrs = await getAttrsOnly<{ well: Ome.Well }>(grp, wellPath);
+      // Fields are by index and we assume at least 1 per Well
+      return join(wellPath, wellAttrs.well.images[0].path);
+    }
+    const imgPaths = await Promise.all(wellPaths.map(getImgPath));
+    return imgPaths;
+  } else {
+    return [];
   }
+}
 
-  const rows = plateAttrs.rows.map((row) => row.name);
-  const columns = plateAttrs.columns.map((row) => row.name);
+export async function loadCollection(
+  config: ImageLayerConfig,
+  grp: ZarrGroup,
+  omeAttrs: Ome.Attrs
+): Promise<SourceData> {
+  const imagePaths = await getImagePaths(grp, omeAttrs);
+
+  let displayName = 'Collection';
+  let rows: string[];
+  let columns: string[];
+  let colCount: number;
+  let rowCount: number;
+  if ('plate' in omeAttrs) {
+    const plateAttrs = omeAttrs.plate;
+    if (!('columns' in plateAttrs) || !('rows' in plateAttrs)) {
+      throw Error(`Plate .zattrs missing columns or rows`);
+    }
+    rows = plateAttrs.rows.map((row) => row.name);
+    columns = plateAttrs.columns.map((row) => row.name);
+    displayName = plateAttrs.name || 'Collection';
+    colCount = columns.length;
+    rowCount = rows.length;
+  } else {
+    const imgCount = imagePaths.length;
+    colCount = Math.ceil(Math.sqrt(imgCount));
+    rowCount = Math.ceil(imgCount / colCount);
+  }
 
-  // Fields are by index and we assume at least 1 per Well
-  const wellPaths = plateAttrs.wells.map((well) => well.path);
+  function getImgSource(source: string, row: number, column: number) {
+    if (rows && columns) {
+      return join(source, rows[row], columns[column]);
+    } else {
+      return join(source, imagePaths[row * colCount + column]);
+    }
+  }
 
-  // Use first image as proxy for others.
-  const wellAttrs = await getAttrsOnly<{ well: Ome.Well }>(grp, wellPaths[0]);
-  if (!('well' in wellAttrs)) {
-    throw Error('Path for image is not valid, not a well.');
+  function getGridCoord(imgPath: string) {
+    let row, col, name;
+    if (rows && columns) {
+      const [rowName, colName] = imgPath.split('/');
+      row = rows.indexOf(rowName);
+      col = columns.indexOf(colName);
+      name = `${rowName}${colName}`;
+    } else {
+      const imgIndex = imagePaths?.indexOf(imgPath);
+      row = Math.floor(imgIndex / colCount);
+      col = imgIndex - row * colCount;
+      name = imgPath;
+    }
+    return { row, col, name };
   }
 
-  const imgPath = wellAttrs.well.images[0].path;
-  const imgAttrs = (await grp.getItem(join(wellPaths[0], imgPath)).then((g) => g.attrs.asObject())) as Ome.Attrs;
+  const imgPath = imagePaths[0];
+  const imgAttrs = (await grp.getItem(imgPath).then((g) => g.attrs.asObject())) as Ome.Attrs;
   if (!('omero' in imgAttrs)) {
     throw Error('Path for image is not valid.');
   }
@@ -133,16 +186,10 @@ export async function loadPlate(config: ImageLayerConfig, grp: ZarrGroup, plateA
   const { datasets } = imgAttrs.multiscales[0];
   const resolution = datasets[datasets.length - 1].path;
 
-  async function getImgPath(wellPath: string) {
-    const wellAttrs = await getAttrsOnly<{ well: Ome.Well }>(grp, wellPath);
-    return join(wellPath, wellAttrs.well.images[0].path);
-  }
-  const wellImagePaths = await Promise.all(wellPaths.map(getImgPath));
-
   // Create loader for every Well. Some loaders may be undefined if Wells are missing.
   const mapper = ([key, path]: string[]) => grp.getItem(path).then((arr) => [key, arr]) as Promise<[string, ZarrArray]>;
   const promises = await pMap(
-    wellImagePaths.map((p) => [p, join(p, resolution)]),
+    imagePaths.map((p) => [p, join(p, resolution)]),
     mapper,
     { concurrency: 10 }
   );
@@ -151,11 +198,11 @@ export async function loadPlate(config: ImageLayerConfig, grp: ZarrGroup, plateA
   const meta = parseOmeroMeta(imgAttrs.omero, axis_labels);
   const tileSize = guessTileSize(data[0][1]);
   const loaders = data.map((d) => {
-    const [row, col] = d[0].split('/');
+    const coord = getGridCoord(d[0]);
     return {
-      name: `${row}${col}`,
-      row: rows.indexOf(row),
-      col: columns.indexOf(col),
+      name: coord.name,
+      row: coord.row,
+      col: coord.col,
       loader: new ZarrPixelSource(d[1], axis_labels, tileSize),
     };
   });
@@ -172,9 +219,9 @@ export async function loadPlate(config: ImageLayerConfig, grp: ZarrGroup, plateA
       colormap: config.colormap ?? '',
       opacity: config.opacity ?? 1,
     },
-    name: plateAttrs.name || 'Plate',
-    rows: rows.length,
-    columns: columns.length,
+    name: displayName,
+    rows: rowCount,
+    columns: colCount,
   };
   // Us onClick from image config or Open Well in new window
   sourceData.onClick = (info: any) => {
@@ -185,7 +232,7 @@ export async function loadPlate(config: ImageLayerConfig, grp: ZarrGroup, plateA
     const { row, column } = gridCoord;
     let imgSource = undefined;
     if (typeof config.source === 'string' && grp.path && !isNaN(row) && !isNaN(column)) {
-      imgSource = join(config.source, rows[row], columns[column]);
+      imgSource = getImgSource(config.source, row, column);
     }
     if (config.onClick) {
       delete info.layer;

From 0a08418f6400f2012eef08e53eda485e00add735 Mon Sep 17 00:00:00 2001
From: William Moore <w.moore@dundee.ac.uk>
Date: Tue, 5 Oct 2021 09:56:23 +0100
Subject: [PATCH 2/3] Remove support for source=plate/acquisition/

This was only a TEMP format and never made it to the HCS spec. HCS images are now
/plate/A/1/0/ and the acquisition is not included in the URL
---
 src/io.ts | 11 -----------
 1 file changed, 11 deletions(-)

diff --git a/src/io.ts b/src/io.ts
index f201642b..e0cceeea 100644
--- a/src/io.ts
+++ b/src/io.ts
@@ -123,17 +123,6 @@ export async function createSourceData(config: ImageLayerConfig): Promise<Source
       return loadOmeroMultiscales(config, node, attrs);
     }
 
-    if (Object.keys(attrs).length === 0 && node.path) {
-      // No rootAttrs in this group.
-      // if url is to a plate/acquisition/ check parent dir for 'plate' zattrs
-      const parentPath = node.path.slice(0, node.path.lastIndexOf('/'));
-      const parent = await openGroup(node.store, parentPath);
-      const parentAttrs = (await parent.attrs.asObject()) as Ome.Attrs;
-      if ('plate' in parentAttrs) {
-        return loadPlate(config, parent, parentAttrs.plate);
-      }
-    }
-
     if (!('multiscales' in attrs)) {
       throw Error('Group is missing multiscales specification.');
     }

From e307782ee35ac5a723d321bd9624300e8fd2ac88 Mon Sep 17 00:00:00 2001
From: William Moore <w.moore@dundee.ac.uk>
Date: Tue, 5 Oct 2021 10:05:37 +0100
Subject: [PATCH 3/3] Fix types

---
 types/ome.ts | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/types/ome.ts b/types/ome.ts
index c4c52ce3..ac3e5236 100644
--- a/types/ome.ts
+++ b/types/ome.ts
@@ -56,12 +56,17 @@ declare module Ome {
     wells: { path: string }[];
   }
 
+  interface Collection {
+    images: {};
+  }
+
   interface Well {
     images: { path: string; acquisition?: number }[];
     version: Version;
   }
 
   type Attrs =
+    | { collection: Collection }
     | { multiscales: Multiscale[] }
     | { omero: Omero; multiscales: Multiscale[] }
     | { plate: Plate }