@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.
Files changed (100) hide show
  1. package/dist/esm/displayset/BaseDisplaySet.d.ts +3 -4
  2. package/dist/esm/displayset/BaseDisplaySet.js +1 -5
  3. package/dist/esm/displayset/IDisplaySet.d.ts +9 -3
  4. package/dist/esm/displayset/ImageStackDisplaySet.d.ts +1 -3
  5. package/dist/esm/displayset/ImageStackDisplaySet.js +0 -1
  6. package/dist/esm/displayset/__fixtures__/scenarioInstances.d.ts +12 -0
  7. package/dist/esm/displayset/__fixtures__/scenarioInstances.js +210 -0
  8. package/dist/esm/displayset/compileSplitRules.d.ts +3 -0
  9. package/dist/esm/displayset/compileSplitRules.js +197 -0
  10. package/dist/esm/displayset/compiledDisplaySetRules.d.ts +3 -0
  11. package/dist/esm/displayset/compiledDisplaySetRules.js +3 -0
  12. package/dist/esm/displayset/createDisplaySetFromGroup.js +54 -19
  13. package/dist/esm/displayset/displaySetContext.d.ts +150 -0
  14. package/dist/esm/displayset/displaySetContext.js +918 -0
  15. package/dist/esm/displayset/displaySetRules.d.ts +5 -0
  16. package/dist/esm/displayset/displaySetRules.js +142 -0
  17. package/dist/esm/displayset/displaySetRules.test.d.ts +1 -0
  18. package/dist/esm/displayset/displaySetRules.test.js +1306 -0
  19. package/dist/esm/displayset/displaySetRulesTypes.d.ts +34 -0
  20. package/dist/esm/displayset/displaySetRulesTypes.js +0 -0
  21. package/dist/esm/displayset/displayset.test.js +422 -130
  22. package/dist/esm/displayset/groupInstancesBySplitRules.d.ts +8 -2
  23. package/dist/esm/displayset/groupInstancesBySplitRules.js +174 -17
  24. package/dist/esm/displayset/index.d.ts +14 -4
  25. package/dist/esm/displayset/index.js +10 -3
  26. package/dist/esm/displayset/isEcgInstance.d.ts +2 -1
  27. package/dist/esm/displayset/isEcgInstance.js +4 -9
  28. package/dist/esm/displayset/isImageInstance.d.ts +2 -1
  29. package/dist/esm/displayset/isImageInstance.js +4 -44
  30. package/dist/esm/displayset/isReconstructableInstances.d.ts +2 -0
  31. package/dist/esm/displayset/isReconstructableInstances.js +91 -0
  32. package/dist/esm/displayset/isVideoInstance.d.ts +2 -1
  33. package/dist/esm/displayset/isVideoInstance.js +4 -30
  34. package/dist/esm/displayset/isWsiInstance.d.ts +2 -1
  35. package/dist/esm/displayset/isWsiInstance.js +4 -6
  36. package/dist/esm/displayset/metadataContext.d.ts +13 -0
  37. package/dist/esm/displayset/metadataContext.js +42 -0
  38. package/dist/esm/displayset/metadataContext.test.d.ts +1 -0
  39. package/dist/esm/displayset/metadataContext.test.js +62 -0
  40. package/dist/esm/displayset/scenarioFixtures.test.d.ts +1 -0
  41. package/dist/esm/displayset/scenarioFixtures.test.js +206 -0
  42. package/dist/esm/displayset/seriesContext.test.d.ts +1 -0
  43. package/dist/esm/displayset/seriesContext.test.js +238 -0
  44. package/dist/esm/displayset/seriesFunctions.d.ts +29 -0
  45. package/dist/esm/displayset/seriesFunctions.js +327 -0
  46. package/dist/esm/displayset/sopClassUidLists.d.ts +14 -0
  47. package/dist/esm/displayset/sopClassUidLists.js +100 -0
  48. package/dist/esm/displayset/sortInstances.d.ts +10 -0
  49. package/dist/esm/displayset/sortInstances.js +33 -0
  50. package/dist/esm/displayset/sortInstances.test.d.ts +1 -0
  51. package/dist/esm/displayset/sortInstances.test.js +56 -0
  52. package/dist/esm/displayset/splitImageIdsBySplitRules.d.ts +3 -3
  53. package/dist/esm/displayset/splitImageIdsBySplitRules.js +5 -5
  54. package/dist/esm/displayset/splitRuleSchema.d.ts +11 -0
  55. package/dist/esm/displayset/splitRuleSchema.js +168 -0
  56. package/dist/esm/displayset/splitRuleSet.d.ts +12 -0
  57. package/dist/esm/displayset/splitRuleSet.js +47 -0
  58. package/dist/esm/displayset/splitRuleSet.test.d.ts +1 -0
  59. package/dist/esm/displayset/splitRuleSet.test.js +119 -0
  60. package/dist/esm/displayset/types.d.ts +37 -4
  61. package/dist/esm/displayset/types.js +15 -0
  62. package/dist/esm/index.d.ts +4 -2
  63. package/dist/esm/index.js +2 -1
  64. package/dist/esm/safeFunctions/compile.d.ts +31 -0
  65. package/dist/esm/safeFunctions/compile.js +142 -0
  66. package/dist/esm/safeFunctions/compile.test.d.ts +1 -0
  67. package/dist/esm/safeFunctions/compile.test.js +82 -0
  68. package/dist/esm/safeFunctions/expression/compiler.d.ts +23 -0
  69. package/dist/esm/safeFunctions/expression/compiler.js +481 -0
  70. package/dist/esm/safeFunctions/expression/expression.test.d.ts +1 -0
  71. package/dist/esm/safeFunctions/expression/expression.test.js +325 -0
  72. package/dist/esm/safeFunctions/expression/index.d.ts +4 -0
  73. package/dist/esm/safeFunctions/expression/index.js +3 -0
  74. package/dist/esm/safeFunctions/expression/parser.d.ts +56 -0
  75. package/dist/esm/safeFunctions/expression/parser.js +248 -0
  76. package/dist/esm/safeFunctions/expression/tokenizer.d.ts +29 -0
  77. package/dist/esm/safeFunctions/expression/tokenizer.js +198 -0
  78. package/dist/esm/safeFunctions/index.d.ts +5 -0
  79. package/dist/esm/safeFunctions/index.js +4 -0
  80. package/dist/esm/safeFunctions/matchTests.d.ts +16 -0
  81. package/dist/esm/safeFunctions/matchTests.js +203 -0
  82. package/dist/esm/safeFunctions/schema.d.ts +72 -0
  83. package/dist/esm/safeFunctions/schema.js +361 -0
  84. package/dist/esm/safeFunctions/types.d.ts +47 -0
  85. package/dist/esm/safeFunctions/types.js +0 -0
  86. package/dist/esm/safeFunctions/values.d.ts +6 -0
  87. package/dist/esm/safeFunctions/values.js +71 -0
  88. package/dist/esm/utilities/Tags.d.ts +1 -0
  89. package/dist/esm/utilities/Tags.js +14 -2
  90. package/dist/esm/utilities/index.d.ts +1 -1
  91. package/dist/esm/utilities/index.js +1 -1
  92. package/dist/esm/utilities/toNumber.d.ts +1 -1
  93. package/dist/esm/utilities/toNumber.js +1 -1
  94. package/dist/esm/version.d.ts +1 -1
  95. package/dist/esm/version.js +1 -1
  96. package/package.json +2 -2
  97. package/dist/esm/displayset/defaultDisplaySetSplitRules.d.ts +0 -2
  98. package/dist/esm/displayset/defaultDisplaySetSplitRules.js +0 -93
  99. package/dist/esm/displayset/viewportTypes.d.ts +0 -4
  100. package/dist/esm/displayset/viewportTypes.js +0 -13
@@ -1,16 +1,15 @@
1
1
  import type { IDisplaySet } from './IDisplaySet.js';
2
- import type { NaturalizedInstance, ViewportTypeHint } from './types.js';
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
- viewportTypes: readonly ViewportTypeHint[];
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.viewportTypes = options.viewportTypes?.length
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, ViewportTypeHint } from './types.js';
1
+ import type { NaturalizedInstance } from './types.js';
2
2
  export interface IDisplaySet {
3
3
  displaySetId: string;
4
- viewportTypes: readonly ViewportTypeHint[];
5
- preferredViewportType: ViewportTypeHint;
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, ViewportTypeHint } from './types.js';
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
+ }
@@ -0,0 +1,3 @@
1
+ import type { SplitRuleSet } from './types.js';
2
+ export declare const compiledDisplaySetRules: SplitRuleSet;
3
+ export declare const defaultDisplaySetSplitRules: SplitRuleSet;
@@ -0,0 +1,3 @@
1
+ import { createDisplaySetSplitRules, displaySetRules } from './displaySetRules.js';
2
+ export const compiledDisplaySetRules = createDisplaySetSplitRules(displaySetRules);
3
+ export const defaultDisplaySetSplitRules = compiledDisplaySetRules;
@@ -1,14 +1,17 @@
1
1
  import { BaseDisplaySet } from './BaseDisplaySet.js';
2
2
  import { ImageStackDisplaySet } from './ImageStackDisplaySet.js';
3
- import { isEcgInstance } from './isEcgInstance.js';
4
- import { isVideoInstance } from './isVideoInstance.js';
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 applyCustomAttributes(displaySet, group, viewportTypes, options) {
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
- const attributes = matchedRule.customAttributes({ instance: first, isMultiFrame, sopClassUids, viewportTypes }, {
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 viewportTypes = getViewportTypesForGroup(group);
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 (first &&
67
- (isVideoInstance(first) || isEcgInstance(first) || isWsiInstance(first))) {
68
- const imageIds = instances.map((i) => i.imageId).filter(Boolean);
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 ?? imageIds,
74
- underlyingImageIds: imageIds,
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
- applyCustomAttributes(displaySet, group, viewportTypes, options);
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
  }