@cornerstonejs/metadata 5.13.2 → 5.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/displayset/BaseDisplaySet.d.ts +3 -4
- package/dist/esm/displayset/BaseDisplaySet.js +1 -5
- package/dist/esm/displayset/IDisplaySet.d.ts +9 -3
- package/dist/esm/displayset/ImageStackDisplaySet.d.ts +1 -3
- package/dist/esm/displayset/ImageStackDisplaySet.js +0 -1
- package/dist/esm/displayset/__fixtures__/scenarioInstances.d.ts +12 -0
- package/dist/esm/displayset/__fixtures__/scenarioInstances.js +210 -0
- package/dist/esm/displayset/compileSplitRules.d.ts +3 -0
- package/dist/esm/displayset/compileSplitRules.js +197 -0
- package/dist/esm/displayset/compiledDisplaySetRules.d.ts +3 -0
- package/dist/esm/displayset/compiledDisplaySetRules.js +3 -0
- package/dist/esm/displayset/createDisplaySetFromGroup.js +54 -19
- package/dist/esm/displayset/displaySetContext.d.ts +150 -0
- package/dist/esm/displayset/displaySetContext.js +918 -0
- package/dist/esm/displayset/displaySetRules.d.ts +5 -0
- package/dist/esm/displayset/displaySetRules.js +142 -0
- package/dist/esm/displayset/displaySetRules.test.d.ts +1 -0
- package/dist/esm/displayset/displaySetRules.test.js +1306 -0
- package/dist/esm/displayset/displaySetRulesTypes.d.ts +34 -0
- package/dist/esm/displayset/displaySetRulesTypes.js +0 -0
- package/dist/esm/displayset/displayset.test.js +422 -130
- package/dist/esm/displayset/groupInstancesBySplitRules.d.ts +8 -2
- package/dist/esm/displayset/groupInstancesBySplitRules.js +174 -17
- package/dist/esm/displayset/index.d.ts +14 -4
- package/dist/esm/displayset/index.js +10 -3
- package/dist/esm/displayset/isEcgInstance.d.ts +2 -1
- package/dist/esm/displayset/isEcgInstance.js +4 -9
- package/dist/esm/displayset/isImageInstance.d.ts +2 -1
- package/dist/esm/displayset/isImageInstance.js +4 -44
- package/dist/esm/displayset/isReconstructableInstances.d.ts +2 -0
- package/dist/esm/displayset/isReconstructableInstances.js +91 -0
- package/dist/esm/displayset/isVideoInstance.d.ts +2 -1
- package/dist/esm/displayset/isVideoInstance.js +4 -30
- package/dist/esm/displayset/isWsiInstance.d.ts +2 -1
- package/dist/esm/displayset/isWsiInstance.js +4 -6
- package/dist/esm/displayset/metadataContext.d.ts +13 -0
- package/dist/esm/displayset/metadataContext.js +42 -0
- package/dist/esm/displayset/metadataContext.test.d.ts +1 -0
- package/dist/esm/displayset/metadataContext.test.js +62 -0
- package/dist/esm/displayset/scenarioFixtures.test.d.ts +1 -0
- package/dist/esm/displayset/scenarioFixtures.test.js +206 -0
- package/dist/esm/displayset/seriesContext.test.d.ts +1 -0
- package/dist/esm/displayset/seriesContext.test.js +238 -0
- package/dist/esm/displayset/seriesFunctions.d.ts +29 -0
- package/dist/esm/displayset/seriesFunctions.js +327 -0
- package/dist/esm/displayset/sopClassUidLists.d.ts +14 -0
- package/dist/esm/displayset/sopClassUidLists.js +100 -0
- package/dist/esm/displayset/sortInstances.d.ts +10 -0
- package/dist/esm/displayset/sortInstances.js +33 -0
- package/dist/esm/displayset/sortInstances.test.d.ts +1 -0
- package/dist/esm/displayset/sortInstances.test.js +56 -0
- package/dist/esm/displayset/splitImageIdsBySplitRules.d.ts +3 -3
- package/dist/esm/displayset/splitImageIdsBySplitRules.js +5 -5
- package/dist/esm/displayset/splitRuleSchema.d.ts +11 -0
- package/dist/esm/displayset/splitRuleSchema.js +168 -0
- package/dist/esm/displayset/splitRuleSet.d.ts +12 -0
- package/dist/esm/displayset/splitRuleSet.js +47 -0
- package/dist/esm/displayset/splitRuleSet.test.d.ts +1 -0
- package/dist/esm/displayset/splitRuleSet.test.js +119 -0
- package/dist/esm/displayset/types.d.ts +37 -4
- package/dist/esm/displayset/types.js +15 -0
- package/dist/esm/index.d.ts +4 -2
- package/dist/esm/index.js +2 -1
- package/dist/esm/safeFunctions/compile.d.ts +31 -0
- package/dist/esm/safeFunctions/compile.js +142 -0
- package/dist/esm/safeFunctions/compile.test.d.ts +1 -0
- package/dist/esm/safeFunctions/compile.test.js +82 -0
- package/dist/esm/safeFunctions/expression/compiler.d.ts +23 -0
- package/dist/esm/safeFunctions/expression/compiler.js +481 -0
- package/dist/esm/safeFunctions/expression/expression.test.d.ts +1 -0
- package/dist/esm/safeFunctions/expression/expression.test.js +325 -0
- package/dist/esm/safeFunctions/expression/index.d.ts +4 -0
- package/dist/esm/safeFunctions/expression/index.js +3 -0
- package/dist/esm/safeFunctions/expression/parser.d.ts +56 -0
- package/dist/esm/safeFunctions/expression/parser.js +248 -0
- package/dist/esm/safeFunctions/expression/tokenizer.d.ts +29 -0
- package/dist/esm/safeFunctions/expression/tokenizer.js +198 -0
- package/dist/esm/safeFunctions/index.d.ts +5 -0
- package/dist/esm/safeFunctions/index.js +4 -0
- package/dist/esm/safeFunctions/matchTests.d.ts +16 -0
- package/dist/esm/safeFunctions/matchTests.js +203 -0
- package/dist/esm/safeFunctions/schema.d.ts +72 -0
- package/dist/esm/safeFunctions/schema.js +361 -0
- package/dist/esm/safeFunctions/types.d.ts +47 -0
- package/dist/esm/safeFunctions/types.js +0 -0
- package/dist/esm/safeFunctions/values.d.ts +6 -0
- package/dist/esm/safeFunctions/values.js +71 -0
- package/dist/esm/utilities/Tags.d.ts +1 -0
- package/dist/esm/utilities/Tags.js +14 -2
- package/dist/esm/utilities/index.d.ts +1 -1
- package/dist/esm/utilities/index.js +1 -1
- package/dist/esm/utilities/toNumber.d.ts +1 -1
- package/dist/esm/utilities/toNumber.js +1 -1
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/package.json +2 -2
- package/dist/esm/displayset/defaultDisplaySetSplitRules.d.ts +0 -2
- package/dist/esm/displayset/defaultDisplaySetSplitRules.js +0 -93
- package/dist/esm/displayset/viewportTypes.d.ts +0 -4
- package/dist/esm/displayset/viewportTypes.js +0 -13
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import type { IDisplaySet } from './IDisplaySet.js';
|
|
2
|
-
import type { NaturalizedInstance
|
|
2
|
+
import type { NaturalizedInstance } from './types.js';
|
|
3
3
|
export type BaseDisplaySetOptions = {
|
|
4
4
|
displaySetId: string;
|
|
5
|
-
viewportTypes?: readonly ViewportTypeHint[];
|
|
6
5
|
instances?: NaturalizedInstance[];
|
|
7
6
|
imageIds?: Iterable<string>;
|
|
8
7
|
underlyingImageIds?: Iterable<string>;
|
|
8
|
+
isDisplayable?: boolean;
|
|
9
9
|
};
|
|
10
10
|
export declare class BaseDisplaySet implements IDisplaySet {
|
|
11
11
|
displaySetId: string;
|
|
12
|
-
|
|
13
|
-
preferredViewportType: ViewportTypeHint;
|
|
12
|
+
isDisplayable: boolean;
|
|
14
13
|
readonly instances: readonly NaturalizedInstance[];
|
|
15
14
|
readonly imageIds: readonly string[];
|
|
16
15
|
readonly underlyingImageIds: readonly string[];
|
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
import { getPreferredViewportType } from './viewportTypes.js';
|
|
2
1
|
export class BaseDisplaySet {
|
|
3
2
|
constructor(options) {
|
|
4
3
|
this.displaySetId = options.displaySetId;
|
|
5
|
-
this.
|
|
6
|
-
? [...options.viewportTypes]
|
|
7
|
-
: ['stack'];
|
|
8
|
-
this.preferredViewportType = getPreferredViewportType(this.viewportTypes);
|
|
4
|
+
this.isDisplayable = options.isDisplayable ?? true;
|
|
9
5
|
this.instances = [...(options.instances ?? [])];
|
|
10
6
|
this.imageIds = [...(options.imageIds ?? [])];
|
|
11
7
|
this.underlyingImageIds = [...(options.underlyingImageIds ?? [])];
|
|
@@ -1,13 +1,19 @@
|
|
|
1
|
-
import type { NaturalizedInstance
|
|
1
|
+
import type { NaturalizedInstance } from './types.js';
|
|
2
2
|
export interface IDisplaySet {
|
|
3
3
|
displaySetId: string;
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
splitKey?: string;
|
|
5
|
+
splitRuleId?: string;
|
|
6
|
+
splitGroupId?: string;
|
|
7
|
+
handler?: string;
|
|
8
|
+
viewportTypes?: readonly string[];
|
|
9
|
+
isDisplayable: boolean;
|
|
6
10
|
instances: readonly NaturalizedInstance[];
|
|
7
11
|
imageIds: readonly string[];
|
|
8
12
|
underlyingImageIds: readonly string[];
|
|
9
13
|
isMultiFrame?: boolean;
|
|
10
14
|
isClip?: boolean;
|
|
15
|
+
isReconstructable?: boolean;
|
|
11
16
|
numImageFrames?: number;
|
|
12
17
|
splitNumber?: number;
|
|
18
|
+
sopClassUids?: readonly string[];
|
|
13
19
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BaseDisplaySet, type BaseDisplaySetOptions } from './BaseDisplaySet.js';
|
|
2
|
-
import type { NaturalizedInstance
|
|
2
|
+
import type { NaturalizedInstance } from './types.js';
|
|
3
3
|
export type ImageStackDisplaySetOptions = Omit<BaseDisplaySetOptions, 'imageIds' | 'underlyingImageIds'> & {
|
|
4
4
|
instances?: NaturalizedInstance[];
|
|
5
5
|
imageIds?: Iterable<string>;
|
|
@@ -10,11 +10,9 @@ export declare class ImageStackDisplaySet extends BaseDisplaySet {
|
|
|
10
10
|
constructor(options: ImageStackDisplaySetOptions);
|
|
11
11
|
static fromInstances(instances: NaturalizedInstance[], options?: {
|
|
12
12
|
displaySetId?: string;
|
|
13
|
-
viewportTypes?: readonly ViewportTypeHint[];
|
|
14
13
|
imageIds?: Iterable<string>;
|
|
15
14
|
}): ImageStackDisplaySet;
|
|
16
15
|
static fromImageIds(imageIds: string[], getNaturalizedInstance: (imageId: string) => NaturalizedInstance | undefined, options?: {
|
|
17
16
|
displaySetId?: string;
|
|
18
|
-
viewportTypes?: readonly ViewportTypeHint[];
|
|
19
17
|
}): ImageStackDisplaySet;
|
|
20
18
|
}
|
|
@@ -40,7 +40,6 @@ export class ImageStackDisplaySet extends BaseDisplaySet {
|
|
|
40
40
|
`display-set-${instances[0]?.imageId ?? 'unknown'}`;
|
|
41
41
|
return new ImageStackDisplaySet({
|
|
42
42
|
displaySetId,
|
|
43
|
-
viewportTypes: options?.viewportTypes ?? ['stack', 'volume', 'volume3d'],
|
|
44
43
|
instances,
|
|
45
44
|
imageIds: options?.imageIds,
|
|
46
45
|
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { NaturalizedInstance } from '../types.js';
|
|
2
|
+
export declare function geDiffusionSeries(): NaturalizedInstance[];
|
|
3
|
+
export declare function t2SeriesWithZeroBValue(): NaturalizedInstance[];
|
|
4
|
+
export declare function ctLocalizers(seriesUid: string): NaturalizedInstance[];
|
|
5
|
+
export declare function ctSeriesWithScouts(): NaturalizedInstance[];
|
|
6
|
+
export declare function usSweepsAndClips(): {
|
|
7
|
+
instances: NaturalizedInstance[];
|
|
8
|
+
sweeps: NaturalizedInstance[][];
|
|
9
|
+
clips: NaturalizedInstance[];
|
|
10
|
+
};
|
|
11
|
+
export declare function fourViewMammogram(): NaturalizedInstance[];
|
|
12
|
+
export declare function uidsOf(instances: readonly NaturalizedInstance[]): unknown[];
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
const SOP_CLASS = {
|
|
2
|
+
CT: '1.2.840.10008.5.1.4.1.1.2',
|
|
3
|
+
MR: '1.2.840.10008.5.1.4.1.1.4',
|
|
4
|
+
US: '1.2.840.10008.5.1.4.1.1.6.1',
|
|
5
|
+
USMultiFrame: '1.2.840.10008.5.1.4.1.1.3.1',
|
|
6
|
+
MGForPresentation: '1.2.840.10008.5.1.4.1.1.1.2',
|
|
7
|
+
};
|
|
8
|
+
const EXPLICIT_VR_LITTLE_ENDIAN = '1.2.840.10008.1.2.1';
|
|
9
|
+
const JPEG_BASELINE = '1.2.840.10008.1.2.4.50';
|
|
10
|
+
const STUDY_UID = '1.2.826.0.1.3680043.8.498.10274110315161201';
|
|
11
|
+
const AXIAL = [1, 0, 0, 0, 1, 0];
|
|
12
|
+
const CORONAL = [1, 0, 0, 0, 0, -1];
|
|
13
|
+
const SAGITTAL = [0, 1, 0, 0, 0, -1];
|
|
14
|
+
let uidCounter = 0;
|
|
15
|
+
function uid(seriesUid) {
|
|
16
|
+
uidCounter += 1;
|
|
17
|
+
return `${seriesUid}.${uidCounter}`;
|
|
18
|
+
}
|
|
19
|
+
function common(seriesUid, fields) {
|
|
20
|
+
return {
|
|
21
|
+
StudyInstanceUID: STUDY_UID,
|
|
22
|
+
SeriesInstanceUID: seriesUid,
|
|
23
|
+
SOPInstanceUID: uid(seriesUid),
|
|
24
|
+
TransferSyntaxUID: EXPLICIT_VR_LITTLE_ENDIAN,
|
|
25
|
+
...fields,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export function geDiffusionSeries() {
|
|
29
|
+
const seriesUid = '1.2.840.113619.2.312.4120.14306937.11001';
|
|
30
|
+
const instances = [];
|
|
31
|
+
for (let position = 0; position < 32; position++) {
|
|
32
|
+
const z = -60 + position * 4;
|
|
33
|
+
for (const bValue of ['0', '1000']) {
|
|
34
|
+
instances.push(common(seriesUid, {
|
|
35
|
+
SOPClassUID: SOP_CLASS.MR,
|
|
36
|
+
Modality: 'MR',
|
|
37
|
+
Manufacturer: 'GE MEDICAL SYSTEMS',
|
|
38
|
+
SeriesDescription: 'DWI ASSET',
|
|
39
|
+
SeriesNumber: 5,
|
|
40
|
+
InstanceNumber: instances.length + 1,
|
|
41
|
+
ImageType: ['ORIGINAL', 'PRIMARY', 'AXIAL'],
|
|
42
|
+
ScanningSequence: 'EP',
|
|
43
|
+
SequenceVariant: ['SK', 'SS'],
|
|
44
|
+
Rows: 256,
|
|
45
|
+
Columns: 256,
|
|
46
|
+
PixelSpacing: [0.9375, 0.9375],
|
|
47
|
+
SliceThickness: 4,
|
|
48
|
+
ImageOrientationPatient: AXIAL,
|
|
49
|
+
ImagePositionPatient: [-120, -128.3, z],
|
|
50
|
+
SliceLocation: z,
|
|
51
|
+
EchoTime: 81.4,
|
|
52
|
+
RepetitionTime: 6000,
|
|
53
|
+
'00431039': [bValue, '8', '0', '0'],
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return instances;
|
|
58
|
+
}
|
|
59
|
+
export function t2SeriesWithZeroBValue() {
|
|
60
|
+
const seriesUid = '1.3.12.2.1107.5.2.43.66012.2026031510214587340112';
|
|
61
|
+
return Array.from({ length: 24 }, (_, index) => {
|
|
62
|
+
const z = -46 + index * 4;
|
|
63
|
+
return common(seriesUid, {
|
|
64
|
+
SOPClassUID: SOP_CLASS.MR,
|
|
65
|
+
Modality: 'MR',
|
|
66
|
+
SeriesDescription: 'AX T2 TSE',
|
|
67
|
+
SeriesNumber: 3,
|
|
68
|
+
InstanceNumber: index + 1,
|
|
69
|
+
ImageType: ['ORIGINAL', 'PRIMARY', 'M', 'NORM', 'DIS2D'],
|
|
70
|
+
ScanningSequence: 'SE',
|
|
71
|
+
Rows: 320,
|
|
72
|
+
Columns: 320,
|
|
73
|
+
PixelSpacing: [0.6875, 0.6875],
|
|
74
|
+
SliceThickness: 4,
|
|
75
|
+
ImageOrientationPatient: AXIAL,
|
|
76
|
+
ImagePositionPatient: [-110, -125.5, z],
|
|
77
|
+
SliceLocation: z,
|
|
78
|
+
DiffusionBValue: 0,
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function ctImage(seriesUid, fields) {
|
|
83
|
+
return common(seriesUid, {
|
|
84
|
+
SOPClassUID: SOP_CLASS.CT,
|
|
85
|
+
Modality: 'CT',
|
|
86
|
+
SeriesDescription: 'CHEST W/O',
|
|
87
|
+
SeriesNumber: 2,
|
|
88
|
+
Rows: 512,
|
|
89
|
+
Columns: 512,
|
|
90
|
+
KVP: 120,
|
|
91
|
+
...fields,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
export function ctLocalizers(seriesUid) {
|
|
95
|
+
return [CORONAL, SAGITTAL].map((orientation, index) => ctImage(seriesUid, {
|
|
96
|
+
InstanceNumber: index + 1,
|
|
97
|
+
ImageType: ['ORIGINAL', 'PRIMARY', 'LOCALIZER'],
|
|
98
|
+
PixelSpacing: [0.976, 0.7],
|
|
99
|
+
ImageOrientationPatient: orientation,
|
|
100
|
+
ImagePositionPatient: [-250, 0, 120],
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
export function ctSeriesWithScouts() {
|
|
104
|
+
const seriesUid = '1.2.392.200036.9116.2.6.1.48.1214833767.1612345678.112233';
|
|
105
|
+
const axial = Array.from({ length: 40 }, (_, index) => {
|
|
106
|
+
const z = 100 - index * 5;
|
|
107
|
+
return ctImage(seriesUid, {
|
|
108
|
+
InstanceNumber: index + 3,
|
|
109
|
+
ImageType: ['ORIGINAL', 'PRIMARY', 'AXIAL'],
|
|
110
|
+
PixelSpacing: [0.703, 0.703],
|
|
111
|
+
SliceThickness: 5,
|
|
112
|
+
ImageOrientationPatient: AXIAL,
|
|
113
|
+
ImagePositionPatient: [-180, -180, z],
|
|
114
|
+
SliceLocation: z,
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
return [...ctLocalizers(seriesUid), ...axial];
|
|
118
|
+
}
|
|
119
|
+
function usImage(seriesUid, time, fields) {
|
|
120
|
+
return common(seriesUid, {
|
|
121
|
+
Modality: 'US',
|
|
122
|
+
SeriesDescription: 'US ABDOMEN',
|
|
123
|
+
SeriesNumber: 1,
|
|
124
|
+
Rows: 600,
|
|
125
|
+
Columns: 800,
|
|
126
|
+
SamplesPerPixel: 3,
|
|
127
|
+
PhotometricInterpretation: 'YBR_FULL_422',
|
|
128
|
+
TransferSyntaxUID: JPEG_BASELINE,
|
|
129
|
+
AcquisitionDateTime: `20260315${time}.000000`,
|
|
130
|
+
SequenceOfUltrasoundRegions: [
|
|
131
|
+
{
|
|
132
|
+
RegionSpatialFormat: 1,
|
|
133
|
+
RegionDataType: 1,
|
|
134
|
+
PhysicalUnitsXDirection: 3,
|
|
135
|
+
PhysicalUnitsYDirection: 3,
|
|
136
|
+
PhysicalDeltaX: 0.0213,
|
|
137
|
+
PhysicalDeltaY: 0.0213,
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
...fields,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
export function usSweepsAndClips() {
|
|
144
|
+
const seriesUid = '1.2.840.113663.1500.1.460388269.2.1.20260315.101500.123';
|
|
145
|
+
let instanceNumber = 0;
|
|
146
|
+
const still = (time) => usImage(seriesUid, time, {
|
|
147
|
+
SOPClassUID: SOP_CLASS.US,
|
|
148
|
+
InstanceNumber: ++instanceNumber,
|
|
149
|
+
ImageType: ['ORIGINAL', 'PRIMARY', 'ABDOMINAL', '0001'],
|
|
150
|
+
});
|
|
151
|
+
const clip = (time) => usImage(seriesUid, time, {
|
|
152
|
+
SOPClassUID: SOP_CLASS.USMultiFrame,
|
|
153
|
+
InstanceNumber: ++instanceNumber,
|
|
154
|
+
ImageType: ['ORIGINAL', 'PRIMARY', 'ABDOMINAL', '0011'],
|
|
155
|
+
NumberOfFrames: 120,
|
|
156
|
+
FrameTime: 33.33,
|
|
157
|
+
CineRate: 30,
|
|
158
|
+
FrameIncrementPointer: 0x00181063,
|
|
159
|
+
});
|
|
160
|
+
const sweep1 = ['101500', '101502', '101504', '101506', '101508'].map(still);
|
|
161
|
+
const clip1 = clip('101540');
|
|
162
|
+
const sweep2 = ['101630', '101632', '101634', '101636'].map(still);
|
|
163
|
+
const clip2 = clip('101720');
|
|
164
|
+
return {
|
|
165
|
+
instances: [...sweep1, clip1, ...sweep2, clip2],
|
|
166
|
+
sweeps: [sweep1, sweep2],
|
|
167
|
+
clips: [clip1, clip2],
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
const MAMMO_VIEW_CODES = {
|
|
171
|
+
CC: {
|
|
172
|
+
CodeValue: 'R-10242',
|
|
173
|
+
CodingSchemeDesignator: 'SRT',
|
|
174
|
+
CodeMeaning: 'cranio-caudal',
|
|
175
|
+
},
|
|
176
|
+
MLO: {
|
|
177
|
+
CodeValue: 'R-10226',
|
|
178
|
+
CodingSchemeDesignator: 'SRT',
|
|
179
|
+
CodeMeaning: 'medio-lateral oblique',
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
export function fourViewMammogram() {
|
|
183
|
+
const seriesUid = '1.2.840.113681.2863050711.1599.3770412345.71';
|
|
184
|
+
const views = [
|
|
185
|
+
['R', 'CC', ['P', 'L']],
|
|
186
|
+
['L', 'CC', ['A', 'R']],
|
|
187
|
+
['R', 'MLO', ['P', 'FL']],
|
|
188
|
+
['L', 'MLO', ['A', 'FR']],
|
|
189
|
+
];
|
|
190
|
+
return views.map(([laterality, view, orientation], index) => common(seriesUid, {
|
|
191
|
+
SOPClassUID: SOP_CLASS.MGForPresentation,
|
|
192
|
+
Modality: 'MG',
|
|
193
|
+
SeriesDescription: 'Screening Mammography',
|
|
194
|
+
SeriesNumber: 71,
|
|
195
|
+
InstanceNumber: index + 1,
|
|
196
|
+
ImageType: ['ORIGINAL', 'PRIMARY'],
|
|
197
|
+
PresentationIntentType: 'FOR PRESENTATION',
|
|
198
|
+
Rows: 4096,
|
|
199
|
+
Columns: 3328,
|
|
200
|
+
ImagerPixelSpacing: [0.07, 0.07],
|
|
201
|
+
ImageLaterality: laterality,
|
|
202
|
+
PatientOrientation: orientation,
|
|
203
|
+
ViewCodeSequence: [
|
|
204
|
+
{ ...MAMMO_VIEW_CODES[view], ViewModifierCodeSequence: [] },
|
|
205
|
+
],
|
|
206
|
+
}));
|
|
207
|
+
}
|
|
208
|
+
export function uidsOf(instances) {
|
|
209
|
+
return instances.map((instance) => instance.SOPInstanceUID);
|
|
210
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { CreateDisplaySetSplitRulesOptions, DisplaySetRules } from './displaySetRulesTypes.js';
|
|
2
|
+
import type { SplitRuleSet } from './types.js';
|
|
3
|
+
export declare function compileDisplaySetSplitRules(rules: DisplaySetRules, options?: CreateDisplaySetSplitRulesOptions): SplitRuleSet;
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { asArrayFirst, toFiniteFirst } from '@cornerstonejs/utils';
|
|
2
|
+
import { at, compileConditionAt, createSite, hasNoValue, invalidAt, isUnsafeKey, matchForm, parseTemplate, readOwn, safeFunctionCompilers, unrecognized, } from '../safeFunctions/index.js';
|
|
3
|
+
import { ContextProgram, displaySetContextFor, isFixedValue, } from './displaySetContext.js';
|
|
4
|
+
import { chainComparators } from './groupInstancesBySplitRules.js';
|
|
5
|
+
import { getMetadataContextProgram } from './metadataContext.js';
|
|
6
|
+
import { validateSplitRuleSetEntry } from './splitRuleSet.js';
|
|
7
|
+
import { splitRuleSchema } from './splitRuleSchema.js';
|
|
8
|
+
const DEFINITION = 'display set rules';
|
|
9
|
+
function instanceReader(compile, name, site, place, field) {
|
|
10
|
+
const read = compile.program.readerAt(compile.layer, name, site, {
|
|
11
|
+
field,
|
|
12
|
+
place,
|
|
13
|
+
});
|
|
14
|
+
const { program } = compile;
|
|
15
|
+
return (instance, context) => read(program.runtimeFrom(context, instance), instance);
|
|
16
|
+
}
|
|
17
|
+
function compileName(value, site, compile) {
|
|
18
|
+
if (typeof value === 'function') {
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
if (typeof value !== 'string') {
|
|
22
|
+
return unrecognized(splitRuleSchema.name, value, site);
|
|
23
|
+
}
|
|
24
|
+
return instanceReader(compile, value, site, 'groupBy');
|
|
25
|
+
}
|
|
26
|
+
function compareSortValues(a, b) {
|
|
27
|
+
const x = toFiniteFirst(a);
|
|
28
|
+
const y = toFiniteFirst(b);
|
|
29
|
+
if (x !== undefined && y !== undefined) {
|
|
30
|
+
return x - y;
|
|
31
|
+
}
|
|
32
|
+
if (hasNoValue(a) || hasNoValue(b)) {
|
|
33
|
+
return Number(hasNoValue(a)) - Number(hasNoValue(b));
|
|
34
|
+
}
|
|
35
|
+
const aText = String(asArrayFirst(a));
|
|
36
|
+
const bText = String(asArrayFirst(b));
|
|
37
|
+
return aText < bText ? -1 : aText > bText ? 1 : 0;
|
|
38
|
+
}
|
|
39
|
+
function compileSortKey(value, site, compile) {
|
|
40
|
+
if (typeof value === 'function') {
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
if (typeof value === 'string') {
|
|
44
|
+
const read = compile.program.readerAt(compile.layer, value, site, {
|
|
45
|
+
place: 'sortBy',
|
|
46
|
+
});
|
|
47
|
+
const { program } = compile;
|
|
48
|
+
return (a, b, context) => {
|
|
49
|
+
const runtime = program.runtimeFrom(context, [a, b]);
|
|
50
|
+
return compareSortValues(read(runtime, a), read(runtime, b));
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
54
|
+
return unrecognized(splitRuleSchema.sortKey, value, site);
|
|
55
|
+
}
|
|
56
|
+
matchForm(splitRuleSchema.sortKey, value, site);
|
|
57
|
+
const name = value.sortFunction;
|
|
58
|
+
const sortFunction = readOwn(compile.sortFunctions, name);
|
|
59
|
+
if (typeof sortFunction !== 'function') {
|
|
60
|
+
return invalidAt(at(site, 'sortFunction'), `unknown sort function "${name}"; known: ${Object.keys(compile.sortFunctions).join(', ') || '(none)'}`);
|
|
61
|
+
}
|
|
62
|
+
return sortFunction;
|
|
63
|
+
}
|
|
64
|
+
function compileAttributeValue(value, site, compile) {
|
|
65
|
+
const { program, layer } = compile;
|
|
66
|
+
if (typeof value === 'function') {
|
|
67
|
+
const scope = { key: `#attribute:${site.path}`, deps: new Map(), layer };
|
|
68
|
+
return (runtime, instances, extras) => value(instances, runtime.env(scope, instances[0], extras));
|
|
69
|
+
}
|
|
70
|
+
if (isFixedValue(value, false)) {
|
|
71
|
+
return () => value;
|
|
72
|
+
}
|
|
73
|
+
if (typeof value === 'string') {
|
|
74
|
+
const read = program.readerAt(layer, value, site, { place: 'attributes' });
|
|
75
|
+
return (runtime, instances, extras) => read(runtime, instances[0], extras);
|
|
76
|
+
}
|
|
77
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
78
|
+
return unrecognized(splitRuleSchema.attributeValue, value, site);
|
|
79
|
+
}
|
|
80
|
+
const [form] = matchForm(splitRuleSchema.attributeValue, value, site);
|
|
81
|
+
const fields = value;
|
|
82
|
+
if (form === 'expression') {
|
|
83
|
+
return program.attributeExpression(layer, fields.expression, at(site, 'expression'));
|
|
84
|
+
}
|
|
85
|
+
const templateSite = at(site, 'template');
|
|
86
|
+
const parts = parseTemplate(fields.template, templateSite).map((segment) => 'literal' in segment
|
|
87
|
+
? () => segment.literal
|
|
88
|
+
: program.readerAt(layer, segment.name, templateSite, {
|
|
89
|
+
field: segment.field,
|
|
90
|
+
place: 'attributes',
|
|
91
|
+
}));
|
|
92
|
+
return (runtime, instances, extras) => parts
|
|
93
|
+
.map((part) => {
|
|
94
|
+
const text = part(runtime, instances[0], extras);
|
|
95
|
+
return hasNoValue(text) ? '' : String(text);
|
|
96
|
+
})
|
|
97
|
+
.join('');
|
|
98
|
+
}
|
|
99
|
+
function compileAttributes(attributes, site, compile) {
|
|
100
|
+
if (typeof attributes === 'function') {
|
|
101
|
+
return attributes;
|
|
102
|
+
}
|
|
103
|
+
if (!attributes ||
|
|
104
|
+
typeof attributes !== 'object' ||
|
|
105
|
+
Array.isArray(attributes)) {
|
|
106
|
+
return invalidAt(site, 'expected an object of attribute values, keyed by attribute name, or a function (attributes, options) => Record<string, unknown>', attributes);
|
|
107
|
+
}
|
|
108
|
+
const readers = Object.entries(attributes).map(([key, value]) => {
|
|
109
|
+
const valueSite = at(site, key);
|
|
110
|
+
if (isUnsafeKey(key)) {
|
|
111
|
+
invalidAt(valueSite, `the key "${key}" is not allowed`);
|
|
112
|
+
}
|
|
113
|
+
return [key, compileAttributeValue(value, valueSite, compile)];
|
|
114
|
+
});
|
|
115
|
+
const { program } = compile;
|
|
116
|
+
return (engine, options) => {
|
|
117
|
+
const instances = options.instances ?? [];
|
|
118
|
+
const runtime = program.runtimeFrom({
|
|
119
|
+
series: options.series,
|
|
120
|
+
displaySet: displaySetContextFor(options.series, instances),
|
|
121
|
+
}, instances);
|
|
122
|
+
const extras = {
|
|
123
|
+
splitNumber: options.splitNumber,
|
|
124
|
+
sopClassUids: engine?.sopClassUids,
|
|
125
|
+
};
|
|
126
|
+
const result = {};
|
|
127
|
+
for (const [key, read] of readers) {
|
|
128
|
+
result[key] = instances.length
|
|
129
|
+
? read(runtime, instances, extras)
|
|
130
|
+
: undefined;
|
|
131
|
+
}
|
|
132
|
+
return result;
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function compileList(value, site, compileEntry) {
|
|
136
|
+
if (!Array.isArray(value)) {
|
|
137
|
+
return invalidAt(site, 'expected an array', value);
|
|
138
|
+
}
|
|
139
|
+
return value.map((entry, index) => compileEntry(entry, at(site, index)));
|
|
140
|
+
}
|
|
141
|
+
function compileRule(id, rule, site, program, sortFunctions) {
|
|
142
|
+
matchForm(splitRuleSchema.rule, rule, site);
|
|
143
|
+
const layer = program.layer(id, rule.context, at(site, 'context'));
|
|
144
|
+
const compile = { site, program, layer, sortFunctions };
|
|
145
|
+
const compiled = { id, series: program.seriesHook };
|
|
146
|
+
if (rule.groupId !== undefined) {
|
|
147
|
+
compiled.groupId = rule.groupId;
|
|
148
|
+
}
|
|
149
|
+
if (rule.viewportTypes !== undefined) {
|
|
150
|
+
compiled.viewportTypes = rule.viewportTypes;
|
|
151
|
+
}
|
|
152
|
+
if (rule.handler !== undefined) {
|
|
153
|
+
compiled.handler = rule.handler;
|
|
154
|
+
}
|
|
155
|
+
if (rule.matches !== undefined) {
|
|
156
|
+
const matchesSite = {
|
|
157
|
+
...at(site, 'matches'),
|
|
158
|
+
readName: (name, nameSite, field) => instanceReader(compile, name, nameSite, 'matches', field),
|
|
159
|
+
};
|
|
160
|
+
compiled.matches = compileConditionAt(rule.matches, matchesSite);
|
|
161
|
+
}
|
|
162
|
+
if (rule.groupBy !== undefined) {
|
|
163
|
+
const groupBy = compileList(rule.groupBy, at(site, 'groupBy'), (entry, s) => compileName(entry, s, compile));
|
|
164
|
+
if (groupBy.length) {
|
|
165
|
+
compiled.groupBy = groupBy;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (rule.sortBy !== undefined) {
|
|
169
|
+
const comparators = compileList(rule.sortBy, at(site, 'sortBy'), (entry, s) => compileSortKey(entry, s, compile));
|
|
170
|
+
if (comparators.length) {
|
|
171
|
+
compiled.compareInstances = chainComparators(comparators);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (rule.attributes !== undefined) {
|
|
175
|
+
compiled.customAttributes = compileAttributes(rule.attributes, at(site, 'attributes'), compile);
|
|
176
|
+
}
|
|
177
|
+
return compiled;
|
|
178
|
+
}
|
|
179
|
+
export function compileDisplaySetSplitRules(rules, options = {}) {
|
|
180
|
+
if (!rules || typeof rules !== 'object' || Array.isArray(rules)) {
|
|
181
|
+
throw new Error(`Invalid ${DEFINITION}: the rules must be an object keyed by rule id: ${JSON.stringify(rules)}`);
|
|
182
|
+
}
|
|
183
|
+
const program = getMetadataContextProgram(options.context, {
|
|
184
|
+
seriesFunctions: options.seriesFunctions,
|
|
185
|
+
deniedNames: options.deniedNames,
|
|
186
|
+
definition: DEFINITION,
|
|
187
|
+
});
|
|
188
|
+
const sortFunctions = options.sortFunctions ?? {};
|
|
189
|
+
const compiledSet = {};
|
|
190
|
+
for (const [id, rule] of Object.entries(rules)) {
|
|
191
|
+
validateSplitRuleSetEntry(id, rule);
|
|
192
|
+
const site = createSite({ definition: DEFINITION, path: `rule '${id}'` }, splitRuleSchema, safeFunctionCompilers);
|
|
193
|
+
const { id: _id, ...compiledRule } = compileRule(id, rule, site, program, sortFunctions);
|
|
194
|
+
compiledSet[id] = { ...compiledRule, priority: rule.priority };
|
|
195
|
+
}
|
|
196
|
+
return compiledSet;
|
|
197
|
+
}
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { BaseDisplaySet } from './BaseDisplaySet.js';
|
|
2
2
|
import { ImageStackDisplaySet } from './ImageStackDisplaySet.js';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { isWsiInstance } from './isWsiInstance.js';
|
|
6
|
-
import { getPreferredViewportType, getViewportTypesForGroup, } from './viewportTypes.js';
|
|
3
|
+
import { isUnsafeKey } from '../safeFunctions/schema.js';
|
|
4
|
+
import { isReconstructableInstances } from './isReconstructableInstances.js';
|
|
7
5
|
const RESERVED_ATTRIBUTE_KEYS = new Set([
|
|
8
6
|
'imageIds',
|
|
9
7
|
'underlyingImageIds',
|
|
10
8
|
'instances',
|
|
11
9
|
'displaySetId',
|
|
10
|
+
'splitKey',
|
|
11
|
+
'splitRuleId',
|
|
12
|
+
'splitGroupId',
|
|
13
|
+
'handler',
|
|
14
|
+
'isDisplayable',
|
|
12
15
|
]);
|
|
13
16
|
function isAssignable(target, key) {
|
|
14
17
|
let obj = target;
|
|
@@ -24,36 +27,38 @@ function isAssignable(target, key) {
|
|
|
24
27
|
}
|
|
25
28
|
return true;
|
|
26
29
|
}
|
|
27
|
-
function
|
|
30
|
+
function runCustomAttributes(group, options) {
|
|
28
31
|
const { instances, matchedRule } = group;
|
|
29
32
|
const first = instances[0];
|
|
30
33
|
if (!matchedRule.customAttributes || !first) {
|
|
31
|
-
return;
|
|
34
|
+
return undefined;
|
|
32
35
|
}
|
|
33
36
|
const sopClassUids = [
|
|
34
37
|
...new Set(instances.map((i) => i.SOPClassUID).filter(Boolean)),
|
|
35
38
|
];
|
|
36
39
|
const isMultiFrame = Number(first.NumberOfFrames) > 1;
|
|
37
|
-
|
|
40
|
+
return (matchedRule.customAttributes({ instance: first, isMultiFrame, sopClassUids }, {
|
|
38
41
|
instances,
|
|
39
42
|
splitNumber: options.splitNumber,
|
|
40
43
|
descriptionName: options.descriptionName,
|
|
41
|
-
|
|
44
|
+
series: group.series,
|
|
45
|
+
}) ?? undefined);
|
|
46
|
+
}
|
|
47
|
+
function applyCustomAttributes(displaySet, attributes) {
|
|
42
48
|
if (!attributes) {
|
|
43
49
|
return;
|
|
44
50
|
}
|
|
45
51
|
for (const [key, value] of Object.entries(attributes)) {
|
|
46
|
-
if (RESERVED_ATTRIBUTE_KEYS.has(key)) {
|
|
52
|
+
if (RESERVED_ATTRIBUTE_KEYS.has(key) || isUnsafeKey(key)) {
|
|
47
53
|
continue;
|
|
48
54
|
}
|
|
49
55
|
if (isAssignable(displaySet, key)) {
|
|
50
56
|
displaySet[key] = value;
|
|
51
57
|
}
|
|
52
58
|
}
|
|
53
|
-
displaySet.preferredViewportType = getPreferredViewportType(displaySet.viewportTypes);
|
|
54
59
|
}
|
|
55
60
|
export function createDisplaySetFromGroup(group, options = {}) {
|
|
56
|
-
const
|
|
61
|
+
const attributes = runCustomAttributes(group, options);
|
|
57
62
|
const { instances } = group;
|
|
58
63
|
const baseDisplaySetId = instances[0]?.SeriesInstanceUID ??
|
|
59
64
|
`display-set-${instances[0]?.imageId ?? 'unknown'}`;
|
|
@@ -62,25 +67,55 @@ export function createDisplaySetFromGroup(group, options = {}) {
|
|
|
62
67
|
? `${baseDisplaySetId}:${options.splitNumber}`
|
|
63
68
|
: baseDisplaySetId);
|
|
64
69
|
const first = instances[0];
|
|
70
|
+
const { handler } = group.matchedRule;
|
|
71
|
+
const underlyingImageIds = instances
|
|
72
|
+
.map((i) => i.imageId)
|
|
73
|
+
.filter(Boolean);
|
|
65
74
|
let displaySet;
|
|
66
|
-
if (
|
|
67
|
-
|
|
68
|
-
|
|
75
|
+
if (handler === 'unsupported') {
|
|
76
|
+
displaySet = new BaseDisplaySet({
|
|
77
|
+
displaySetId,
|
|
78
|
+
instances,
|
|
79
|
+
imageIds: [],
|
|
80
|
+
underlyingImageIds,
|
|
81
|
+
isDisplayable: false,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
else if (handler !== undefined && handler !== 'image') {
|
|
69
85
|
displaySet = new BaseDisplaySet({
|
|
70
86
|
displaySetId,
|
|
71
|
-
viewportTypes,
|
|
72
87
|
instances,
|
|
73
|
-
imageIds: options.imageIds ??
|
|
74
|
-
underlyingImageIds
|
|
88
|
+
imageIds: options.imageIds ?? underlyingImageIds,
|
|
89
|
+
underlyingImageIds,
|
|
75
90
|
});
|
|
76
91
|
}
|
|
77
92
|
else {
|
|
78
93
|
displaySet = ImageStackDisplaySet.fromInstances(instances, {
|
|
79
94
|
displaySetId,
|
|
80
|
-
viewportTypes,
|
|
81
95
|
imageIds: options.imageIds,
|
|
82
96
|
});
|
|
83
97
|
}
|
|
84
|
-
|
|
98
|
+
displaySet.splitKey = group.splitKey;
|
|
99
|
+
displaySet.splitRuleId = group.matchedRule.id;
|
|
100
|
+
displaySet.splitGroupId = group.matchedRule.groupId ?? group.matchedRule.id;
|
|
101
|
+
if (handler !== undefined) {
|
|
102
|
+
displaySet.handler = handler;
|
|
103
|
+
}
|
|
104
|
+
if (group.matchedRule.viewportTypes !== undefined) {
|
|
105
|
+
displaySet.viewportTypes = [...group.matchedRule.viewportTypes];
|
|
106
|
+
}
|
|
107
|
+
displaySet.sopClassUids = [
|
|
108
|
+
...new Set(instances.map((i) => i.SOPClassUID).filter(Boolean)),
|
|
109
|
+
];
|
|
110
|
+
if (options.splitNumber !== undefined) {
|
|
111
|
+
displaySet.splitNumber = options.splitNumber;
|
|
112
|
+
}
|
|
113
|
+
displaySet.isMultiFrame ??= Number(first?.NumberOfFrames) > 1;
|
|
114
|
+
applyCustomAttributes(displaySet, attributes);
|
|
115
|
+
if ((handler === undefined || handler === 'image') &&
|
|
116
|
+
typeof displaySet.isReconstructable !== 'boolean') {
|
|
117
|
+
displaySet.isReconstructable =
|
|
118
|
+
!displaySet.isClip && isReconstructableInstances(instances);
|
|
119
|
+
}
|
|
85
120
|
return displaySet;
|
|
86
121
|
}
|