From 4371614b63866fbe0a2a93b3a0a9b8739ebbeba6 Mon Sep 17 00:00:00 2001 From: John Hoffer Date: Tue, 8 Oct 2024 14:52:24 -0400 Subject: [PATCH] Add reactivity with minerva-author-ui@2.0 --- src/components/index.tsx | 3 +- src/components/vivView.tsx | 2 +- src/lib/config.ts | 150 +++++++++++++++++++++++++++++++++++-- src/lib/filesystem.ts | 7 +- src/lib/viv.ts | 52 ++++++++++++- src/main.tsx | 26 +++++-- 6 files changed, 222 insertions(+), 18 deletions(-) diff --git a/src/components/index.tsx b/src/components/index.tsx index 4004be8..a3aa87d 100644 --- a/src/components/index.tsx +++ b/src/components/index.tsx @@ -7,6 +7,7 @@ import { Main } from "./content"; import type { OptSW } from "./waypoint/content"; import type { Waypoint as WaypointType } from "../lib/exhibit"; import type { HashContext } from "../lib/hashUtil"; +import type { Loader } from "../lib/viv"; import type { Exhibit } from "../lib/exhibit"; import type { ConfigGroup } from "../lib/config"; import type { ConfigWaypoint } from "../lib/config"; @@ -15,7 +16,7 @@ import type { ConfigSourceChannel } from "../lib/config"; type Props = HashContext & { in_f: string; - loader: any; + loader: Loader; exhibit: Exhibit; handle: Handle.Dir; title: string; diff --git a/src/components/vivView.tsx b/src/components/vivView.tsx index 1a0ba2e..43f7cf8 100644 --- a/src/components/vivView.tsx +++ b/src/components/vivView.tsx @@ -71,8 +71,8 @@ const VivView = (props: Props) => { const viewerProps = { ...{ - loader, ...shape, + loader: loader.data, ...(settings as any), }, }; diff --git a/src/lib/config.ts b/src/lib/config.ts index 81770cf..15dcd7f 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -1,3 +1,7 @@ +import { reactive } from '@arrow-js/core'; + +import type { Loader } from './viv'; + type ExpandedState = { Expanded: boolean; }; @@ -5,6 +9,7 @@ type GroupState = ExpandedState; type GroupChannelState = ExpandedState; type WaypointState = ExpandedState; +type ID = { ID: string; }; type UUID = { UUID: string; }; type NameProperty = { Name: string; }; type GroupProperties = NameProperty; @@ -19,12 +24,14 @@ type WaypointProperties = NameProperty & { Content: string; }; -type Associations = Record; -type SourceChannelAssociations = Associations< - 'SourceDataType' | 'SourceImage' +type SourceChannelAssociations = Record< + 'SourceDataType', ID +> & Record< + 'SourceImage', UUID >; -type GroupChannelAssociations = Associations< - 'SourceChannel' | 'Group' +type GroupChannelAssociations = Record< + 'SourceChannel' | 'Group', + UUID >; export type ConfigSourceChannel = UUID & { @@ -44,3 +51,136 @@ export type ConfigWaypoint = UUID & { State: WaypointState; Properties: WaypointProperties; }; +interface ExtractChannels { + (loader: Loader): { + SourceChannels: ConfigSourceChannel[]; + GroupChannels: ConfigGroupChannel[]; + Groups: ConfigGroup[]; + } +} + +const asID = (k: string): ID => ({ ID: k }); +const asUUID = (k: string): UUID => ({ UUID: k }); + +const extractChannels: ExtractChannels = (loader) => { + const { Channels, Type } = loader.metadata.Pixels; + const SourceChannels = Channels.map( + (channel, index) => ({ + UUID: crypto.randomUUID(), + Properties: { + Name: channel.Name, + SourceIndex: index, + }, + Associations: { + SourceDataType: asID(Type), + SourceImage: asUUID('TODO') + } + }) + ); + const group_size = 4; + const Groups = [...Array(Math.ceil( + SourceChannels.length / group_size + )).keys()].map( + index => ({ + UUID: crypto.randomUUID(), + State: { Expanded: false }, + Properties: { + Name: `Group ${index}` + } + }) + ) + const GroupChannels = SourceChannels.map( + (channel, index) => ({ + UUID: crypto.randomUUID(), + State: { Expanded: false }, + Properties: { + LowerRange: 0, UpperRange: 65535 + }, + Associations: { + SourceChannel: asUUID(channel.UUID), + Group: asUUID(Groups[ + Math.floor(index / group_size) + ].UUID) + } + }) + ) + return { + SourceChannels, + GroupChannels, + Groups + } +} + +const mutableConfigArrayItem = ( + item, namespace, array, index +) => { + return [ + namespace, new Proxy( + item[namespace], { + has(target, k) { + if (k == '$on') + return true; + return k in target; + }, + get(target, k) { + if (k == '$on') + return () => {}; + return target[k]; + }, + set(target, k, v) { + if (k in target) { + target[k] = v; + array.splice(index, 1, item); + } + return true; + } + } + ) + ]; +} + +const mutableConfigArray = ( + state_array, set_state, +) => { + const methods = [ + 'pop', 'push', 'shift', 'unshift', + 'splice', 'sort', 'reverse' + ]; + const namespaces = [ + /*'State', */'Properties', 'Associations' + ]; + return new Proxy(state_array, { + get(_, key, receiver) { + const item = state_array[key]; + if (methods.includes(String(key))) { + // Let specific array methods set the array state + return new Proxy(item, { + apply(fn, _, ...args) { + const new_state = [...state_array]; + const output = fn.apply(new_state, args); + set_state(new_state); + return output; + } + }); + } + if (typeof key == 'symbol') { + return item; + } + const index = parseInt(key as string); + if (isNaN(index) || typeof item != 'object') { + return item; + } + // Let specific properties be modified + const entries = namespaces.map( + namespace => mutableConfigArrayItem( + item, namespace, receiver, index + ) + ); + return { + ...item, ...Object.fromEntries(entries) + } + } + }); +} + +export { extractChannels, mutableConfigArray } diff --git a/src/lib/filesystem.ts b/src/lib/filesystem.ts index dae7af5..639a088 100644 --- a/src/lib/filesystem.ts +++ b/src/lib/filesystem.ts @@ -2,6 +2,8 @@ import { loadOmeTiff, } from "@hms-dbmi/viv"; +import type { Loader } from './viv'; + type ListDirIn = { handle: Handle.Dir, } @@ -25,11 +27,8 @@ type LoaderIn = { in_f: string, handle: Handle.Dir } -type LoaderOut = { - data: LoaderPlane[] -} interface ToLoader { - (i: LoaderIn): Promise; + (i: LoaderIn): Promise; } export type Selection = { t: number, diff --git a/src/lib/viv.ts b/src/lib/viv.ts index c2666d5..78fa302 100644 --- a/src/lib/viv.ts +++ b/src/lib/viv.ts @@ -12,8 +12,56 @@ type Settings = { colors: Color[]; }; +type Channel = { + ID: string; + SamplesPerPixel: number; + Name: string; +} + +type TiffDatum = { + IFD: number; + PlaneCount: number; + FirstT: number; + FirstC: number; + FirstZ: number; + UUID: { + FileName: string; + }; +} + +type Pixels = { + Channels: Channel[]; + ID: string; + DimensionOrder: string; + Type: string; + SizeT: number; + SizeC: number; + SizeZ: number; + SizeY: number; + SizeX: number; + PhysicalSizeX: number; + PhysicalSizeY: number; + PhysicalSizeXUnit: string; + PhysicalSizeYUnit: string; + PhysicalSizeZUnit: string; + BigEndian: boolean; + TiffData: TiffDatum[]; +} + +type Metadata = { + ID: string; + AquisitionDate: string; + Description: string; + Pixels: Pixels; +} + +export type Loader = { + data: any[]; + metadata: any; +} + export type Config = { - toSettings: (h: HashState, l?: any, g?: any) => Settings; + toSettings: (h: HashState, l?: Loader, g?: any) => Settings; }; const toDefaultSettings = (n) => { @@ -63,7 +111,7 @@ const toSettings = (opts) => { const channels = group?.channels || []; // Defaults if (!loader) return toDefaultSettings(3); - const full_level = loader[0]; + const full_level = loader.data[0]; if (!loader) return toDefaultSettings(3); const { labels, shape } = full_level; const c_idx = labels.indexOf("c"); diff --git a/src/main.tsx b/src/main.tsx index 5ffc1e8..3e21570 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -5,6 +5,9 @@ import { useState, useEffect } from "react"; import { useHash } from "./lib/hashUtil"; import { hasFileSystemAccess, toDir, toLoader } from "./lib/filesystem"; import { isOpts, validate } from './lib/validate'; +import { + extractChannels, mutableConfigArray +} from './lib/config'; import { Upload } from './components/upload'; import { readConfig } from "./lib/exhibit"; import { Index } from "./components"; @@ -57,6 +60,16 @@ const Content = (props: Props) => { const [url, setUrl] = useState(window.location.href); const hashContext = useHash(url, exhibit.stories); const [handle, setHandle] = useState(null); + const [sourceChannels, setSourceChannels] = useState([]); + const [groupChannels, setGroupChannels] = useState([]); + const [groups, setGroups] = useState([]); + const configState = { + configGroups: groups, + configGroupChannels: mutableConfigArray( + groupChannels, setGroupChannels + ), + configSourceChannels: sourceChannels + }; const [loader, setLoader] = useState(null); const [fileName, setFileName] = useState(''); // Create ome-tiff loader @@ -82,7 +95,13 @@ const Content = (props: Props) => { (async () => { if (handle === null) return; const loader = await toLoader({ handle, in_f }); - setLoader(loader.data); + const { + SourceChannels, GroupChannels, Groups + } = extractChannels(loader); + setSourceChannels(SourceChannels); + setGroupChannels(GroupChannels); + setGroups(Groups); + setLoader(loader); setFileName(in_f); })(); } @@ -93,15 +112,12 @@ const Content = (props: Props) => { }); }, []) const { marker_names, title, configWaypoints } = props; - const { - configGroups, configGroupChannels, configSourceChannels - } = props; // Actual image viewer const imager = loader === null ? '' : (