Skip to content

Commit c60a26f

Browse files
committed
Add reactivity with minerva-author-ui@2.0
1 parent 4a7a33c commit c60a26f

File tree

6 files changed

+220
-18
lines changed

6 files changed

+220
-18
lines changed

src/components/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { Main } from "./content";
77
import type { OptSW } from "./waypoint/content";
88
import type { Waypoint as WaypointType } from "../lib/exhibit";
99
import type { HashContext } from "../lib/hashUtil";
10+
import type { Loader } from "../lib/viv";
1011
import type { Exhibit } from "../lib/exhibit";
1112
import type { ConfigGroup } from "../lib/config";
1213
import type { ConfigWaypoint } from "../lib/config";
@@ -15,7 +16,7 @@ import type { ConfigSourceChannel } from "../lib/config";
1516

1617
type Props = HashContext & {
1718
in_f: string;
18-
loader: any;
19+
loader: Loader;
1920
exhibit: Exhibit;
2021
handle: Handle.Dir;
2122
title: string;

src/components/vivView.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,8 @@ const VivView = (props: Props) => {
7171

7272
const viewerProps = {
7373
...{
74-
loader,
7574
...shape,
75+
loader: loader.data,
7676
...(settings as any),
7777
},
7878
};

src/lib/config.ts

Lines changed: 143 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
import type { Loader } from './viv';
2+
13
type ExpandedState = {
24
Expanded: boolean;
35
};
46
type GroupState = ExpandedState;
57
type GroupChannelState = ExpandedState;
68
type WaypointState = ExpandedState;
79

10+
type ID = { ID: string; };
811
type UUID = { UUID: string; };
912
type NameProperty = { Name: string; };
1013
type GroupProperties = NameProperty;
@@ -19,12 +22,14 @@ type WaypointProperties = NameProperty & {
1922
Content: string;
2023
};
2124

22-
type Associations<T extends string> = Record<T, UUID>;
23-
type SourceChannelAssociations = Associations<
24-
'SourceDataType' | 'SourceImage'
25+
type SourceChannelAssociations = Record<
26+
'SourceDataType', ID
27+
> & Record<
28+
'SourceImage', UUID
2529
>;
26-
type GroupChannelAssociations = Associations<
27-
'SourceChannel' | 'Group'
30+
type GroupChannelAssociations = Record<
31+
'SourceChannel' | 'Group',
32+
UUID
2833
>;
2934

3035
export type ConfigSourceChannel = UUID & {
@@ -44,3 +49,136 @@ export type ConfigWaypoint = UUID & {
4449
State: WaypointState;
4550
Properties: WaypointProperties;
4651
};
52+
interface ExtractChannels {
53+
(loader: Loader): {
54+
SourceChannels: ConfigSourceChannel[];
55+
GroupChannels: ConfigGroupChannel[];
56+
Groups: ConfigGroup[];
57+
}
58+
}
59+
60+
const asID = (k: string): ID => ({ ID: k });
61+
const asUUID = (k: string): UUID => ({ UUID: k });
62+
63+
const extractChannels: ExtractChannels = (loader) => {
64+
const { Channels, Type } = loader.metadata.Pixels;
65+
const SourceChannels = Channels.map(
66+
(channel, index) => ({
67+
UUID: crypto.randomUUID(),
68+
Properties: {
69+
Name: channel.Name,
70+
SourceIndex: index,
71+
},
72+
Associations: {
73+
SourceDataType: asID(Type),
74+
SourceImage: asUUID('TODO')
75+
}
76+
})
77+
);
78+
const group_size = 4;
79+
const Groups = [...Array(Math.ceil(
80+
SourceChannels.length / group_size
81+
)).keys()].map(
82+
index => ({
83+
UUID: crypto.randomUUID(),
84+
State: { Expanded: false },
85+
Properties: {
86+
Name: `Group ${index}`
87+
}
88+
})
89+
)
90+
const GroupChannels = SourceChannels.map(
91+
(channel, index) => ({
92+
UUID: crypto.randomUUID(),
93+
State: { Expanded: false },
94+
Properties: {
95+
LowerRange: 0, UpperRange: 65535
96+
},
97+
Associations: {
98+
SourceChannel: asUUID(channel.UUID),
99+
Group: asUUID(Groups[
100+
Math.floor(index / group_size)
101+
].UUID)
102+
}
103+
})
104+
)
105+
return {
106+
SourceChannels,
107+
GroupChannels,
108+
Groups
109+
}
110+
}
111+
112+
const mutableConfigArrayItem = (
113+
item, namespace, array, index
114+
) => {
115+
return [
116+
namespace, new Proxy(
117+
item[namespace], {
118+
has(target, k) {
119+
if (k == '$on')
120+
return true;
121+
return k in target;
122+
},
123+
get(target, k) {
124+
if (k == '$on')
125+
return () => {};
126+
return target[k];
127+
},
128+
set(target, k, v) {
129+
if (k in target) {
130+
target[k] = v;
131+
array.splice(index, 1, item);
132+
}
133+
return true;
134+
}
135+
}
136+
)
137+
];
138+
}
139+
140+
const mutableConfigArray = (
141+
state_array, set_state,
142+
) => {
143+
const methods = [
144+
'pop', 'push', 'shift', 'unshift',
145+
'splice', 'sort', 'reverse'
146+
];
147+
const namespaces = [
148+
/*'State', */'Properties', 'Associations'
149+
];
150+
return new Proxy(state_array, {
151+
get(_, key, receiver) {
152+
const item = state_array[key];
153+
if (methods.includes(String(key))) {
154+
// Let specific array methods set the array state
155+
return new Proxy(item, {
156+
apply(fn, _, ...args) {
157+
const new_state = [...state_array];
158+
const output = fn.apply(new_state, args);
159+
set_state(new_state);
160+
return output;
161+
}
162+
});
163+
}
164+
if (typeof key == 'symbol') {
165+
return item;
166+
}
167+
const index = parseInt(key as string);
168+
if (isNaN(index) || typeof item != 'object') {
169+
return item;
170+
}
171+
// Let specific properties be modified
172+
const entries = namespaces.map(
173+
namespace => mutableConfigArrayItem(
174+
item, namespace, receiver, index
175+
)
176+
);
177+
return {
178+
...item, ...Object.fromEntries(entries)
179+
}
180+
}
181+
});
182+
}
183+
184+
export { extractChannels, mutableConfigArray }

src/lib/filesystem.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import {
22
loadOmeTiff,
33
} from "@hms-dbmi/viv";
44

5+
import type { Loader } from './viv';
6+
57
type ListDirIn = {
68
handle: Handle.Dir,
79
}
@@ -25,11 +27,8 @@ type LoaderIn = {
2527
in_f: string,
2628
handle: Handle.Dir
2729
}
28-
type LoaderOut = {
29-
data: LoaderPlane[]
30-
}
3130
interface ToLoader {
32-
(i: LoaderIn): Promise<LoaderOut>;
31+
(i: LoaderIn): Promise<Loader>;
3332
}
3433
export type Selection = {
3534
t: number,

src/lib/viv.ts

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,56 @@ type Settings = {
1212
colors: Color[];
1313
};
1414

15+
type Channel = {
16+
ID: string;
17+
SamplesPerPixel: number;
18+
Name: string;
19+
}
20+
21+
type TiffDatum = {
22+
IFD: number;
23+
PlaneCount: number;
24+
FirstT: number;
25+
FirstC: number;
26+
FirstZ: number;
27+
UUID: {
28+
FileName: string;
29+
};
30+
}
31+
32+
type Pixels = {
33+
Channels: Channel[];
34+
ID: string;
35+
DimensionOrder: string;
36+
Type: string;
37+
SizeT: number;
38+
SizeC: number;
39+
SizeZ: number;
40+
SizeY: number;
41+
SizeX: number;
42+
PhysicalSizeX: number;
43+
PhysicalSizeY: number;
44+
PhysicalSizeXUnit: string;
45+
PhysicalSizeYUnit: string;
46+
PhysicalSizeZUnit: string;
47+
BigEndian: boolean;
48+
TiffData: TiffDatum[];
49+
}
50+
51+
type Metadata = {
52+
ID: string;
53+
AquisitionDate: string;
54+
Description: string;
55+
Pixels: Pixels;
56+
}
57+
58+
export type Loader = {
59+
data: any[];
60+
metadata: any;
61+
}
62+
1563
export type Config = {
16-
toSettings: (h: HashState, l?: any, g?: any) => Settings;
64+
toSettings: (h: HashState, l?: Loader, g?: any) => Settings;
1765
};
1866

1967
const toDefaultSettings = (n) => {
@@ -63,7 +111,7 @@ const toSettings = (opts) => {
63111
const channels = group?.channels || [];
64112
// Defaults
65113
if (!loader) return toDefaultSettings(3);
66-
const full_level = loader[0];
114+
const full_level = loader.data[0];
67115
if (!loader) return toDefaultSettings(3);
68116
const { labels, shape } = full_level;
69117
const c_idx = labels.indexOf("c");

src/main.tsx

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import { useState, useEffect } from "react";
55
import { useHash } from "./lib/hashUtil";
66
import { hasFileSystemAccess, toDir, toLoader } from "./lib/filesystem";
77
import { isOpts, validate } from './lib/validate';
8+
import {
9+
extractChannels, mutableConfigArray
10+
} from './lib/config';
811
import { Upload } from './components/upload';
912
import { readConfig } from "./lib/exhibit";
1013
import { Index } from "./components";
@@ -57,6 +60,16 @@ const Content = (props: Props) => {
5760
const [url, setUrl] = useState(window.location.href);
5861
const hashContext = useHash(url, exhibit.stories);
5962
const [handle, setHandle] = useState(null);
63+
const [sourceChannels, setSourceChannels] = useState([]);
64+
const [groupChannels, setGroupChannels] = useState([]);
65+
const [groups, setGroups] = useState([]);
66+
const configState = {
67+
configGroups: groups,
68+
configGroupChannels: mutableConfigArray(
69+
groupChannels, setGroupChannels
70+
),
71+
configSourceChannels: sourceChannels
72+
};
6073
const [loader, setLoader] = useState(null);
6174
const [fileName, setFileName] = useState('');
6275
// Create ome-tiff loader
@@ -82,7 +95,13 @@ const Content = (props: Props) => {
8295
(async () => {
8396
if (handle === null) return;
8497
const loader = await toLoader({ handle, in_f });
85-
setLoader(loader.data);
98+
const {
99+
SourceChannels, GroupChannels, Groups
100+
} = extractChannels(loader);
101+
setSourceChannels(SourceChannels);
102+
setGroupChannels(GroupChannels);
103+
setGroups(Groups);
104+
setLoader(loader);
86105
setFileName(in_f);
87106
})();
88107
}
@@ -93,15 +112,12 @@ const Content = (props: Props) => {
93112
});
94113
}, [])
95114
const { marker_names, title, configWaypoints } = props;
96-
const {
97-
configGroups, configGroupChannels, configSourceChannels
98-
} = props;
99115

100116
// Actual image viewer
101117
const imager = loader === null ? '' : (
102118
<Full>
103119
<Index {...{
104-
configGroups, configGroupChannels, configSourceChannels,
120+
...configState,
105121
title, configWaypoints, exhibit, setExhibit, loader,
106122
marker_names, in_f: fileName, handle, ...hashContext
107123
}} />

0 commit comments

Comments
 (0)