@vitessce/vit-s 2.0.2 → 2.0.3

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 (172) hide show
  1. package/dist/index.mjs +84801 -0
  2. package/{dist → dist-tsc}/index.js +2 -2
  3. package/package.json +7 -7
  4. package/src/CallbackPublisher.js +81 -0
  5. package/src/LoadingIndicator.js +34 -0
  6. package/src/TitleInfo.js +240 -0
  7. package/src/VitS.js +180 -0
  8. package/src/VitessceGrid.js +110 -0
  9. package/src/VitessceGrid.test.jsx +48 -0
  10. package/src/Warning.js +62 -0
  11. package/{dist → src}/classNames.js +0 -0
  12. package/src/component-registry.js +21 -0
  13. package/src/data/AbstractLoader.js +46 -0
  14. package/{dist → src}/data/AbstractTwoStepLoader.js +5 -4
  15. package/src/data/LoaderResult.js +8 -0
  16. package/{dist → src}/data/index.js +0 -0
  17. package/src/data/loader-registry.js +19 -0
  18. package/src/data-hook-utils.js +197 -0
  19. package/src/data-hooks.js +354 -0
  20. package/src/errors/AbstractLoaderError.js +14 -0
  21. package/src/errors/DataSourceFetchError.js +15 -0
  22. package/src/errors/DatasetNotFoundError.js +23 -0
  23. package/src/errors/LoaderNotFoundError.js +24 -0
  24. package/src/errors/LoaderValidationError.js +23 -0
  25. package/src/errors/OptionsValidationError.js +25 -0
  26. package/{dist → src}/errors/index.js +0 -0
  27. package/src/file-options-schemas-legacy.js +139 -0
  28. package/src/file-options-schemas-legacy.test.js +138 -0
  29. package/src/file-options-schemas.js +327 -0
  30. package/src/file-options-schemas.test.js +26 -0
  31. package/src/hooks.js +252 -0
  32. package/src/index.js +101 -0
  33. package/src/joint-file-types-legacy.js +298 -0
  34. package/src/joint-file-types-legacy.test.js +412 -0
  35. package/src/joint-file-types.js +171 -0
  36. package/src/joint-file-types.test.js +144 -0
  37. package/{dist/schemas.js → src/json-schemas.js} +6 -1
  38. package/{dist → src}/plugins.js +102 -70
  39. package/src/plugins.test.fixtures.js +78 -0
  40. package/src/plugins.test.js +42 -0
  41. package/src/schemas/config-0.1.0.schema.json +85 -0
  42. package/src/schemas/config-1.0.0.schema.json +733 -0
  43. package/src/schemas/config-1.0.1.schema.json +909 -0
  44. package/src/schemas/config-1.0.10.schema.json +969 -0
  45. package/src/schemas/config-1.0.11.schema.json +990 -0
  46. package/src/schemas/config-1.0.12.schema.json +997 -0
  47. package/src/schemas/config-1.0.13.schema.json +1013 -0
  48. package/src/schemas/config-1.0.14.schema.json +1048 -0
  49. package/src/schemas/config-1.0.15.schema.json +1048 -0
  50. package/src/schemas/config-1.0.16.schema.json +1069 -0
  51. package/src/schemas/config-1.0.16.schema.test.js +32 -0
  52. package/src/schemas/config-1.0.2.schema.json +911 -0
  53. package/src/schemas/config-1.0.3.schema.json +911 -0
  54. package/src/schemas/config-1.0.4.schema.json +949 -0
  55. package/src/schemas/config-1.0.5.schema.json +949 -0
  56. package/src/schemas/config-1.0.6.schema.json +950 -0
  57. package/src/schemas/config-1.0.7.schema.json +950 -0
  58. package/src/schemas/config-1.0.8.schema.json +966 -0
  59. package/src/schemas/config-1.0.9.schema.json +965 -0
  60. package/src/schemas/fixtures/cellSets.good.json +16 -0
  61. package/src/schemas/fixtures/config-1.0.1.bad.json +1 -0
  62. package/src/schemas/fixtures/config-1.0.1.bad.message.json +11 -0
  63. package/src/schemas/fixtures/config-1.0.1.good.json +37 -0
  64. package/src/schemas/fixtures/config-1.0.2.bad.json +1 -0
  65. package/src/schemas/fixtures/config-1.0.2.bad.message.json +11 -0
  66. package/src/schemas/fixtures/config-1.0.2.good.json +38 -0
  67. package/src/schemas/fixtures/config-1.0.3.bad.json +1 -0
  68. package/src/schemas/fixtures/config-1.0.3.bad.message.json +11 -0
  69. package/src/schemas/fixtures/config-1.0.3.good.json +38 -0
  70. package/src/schemas/fixtures/obsSets.bad.json +1 -0
  71. package/src/schemas/fixtures/obsSets.bad.message.json +11 -0
  72. package/src/schemas/fixtures/obsSets.good.json +16 -0
  73. package/src/schemas/fixtures/obsSetsTabular.bad.json +1 -0
  74. package/src/schemas/fixtures/obsSetsTabular.bad.message.json +11 -0
  75. package/src/schemas/fixtures/obsSetsTabular.good.json +26 -0
  76. package/src/schemas/fixtures/raster.bad.json +1 -0
  77. package/src/schemas/fixtures/raster.bad.message.json +11 -0
  78. package/src/schemas/fixtures/raster.good.json +39 -0
  79. package/src/schemas/obsSets.schema.js +195 -0
  80. package/src/schemas/obsSetsTabular.schema.js +39 -0
  81. package/src/schemas/raster.schema.js +121 -0
  82. package/src/schemas/schema-schema.sh +34 -0
  83. package/{dist → src}/schemas/schema.test-old.js +0 -1
  84. package/src/shared-mui/components.js +79 -0
  85. package/src/shared-mui/container.js +212 -0
  86. package/src/shared-mui/styles.js +110 -0
  87. package/src/shared-plot-options/CellColorEncodingOption.js +43 -0
  88. package/src/shared-plot-options/OptionSelect.js +19 -0
  89. package/src/shared-plot-options/OptionsContainer.js +26 -0
  90. package/{dist → src}/shared-plot-options/index.js +0 -0
  91. package/src/shared-plot-options/styles.js +49 -0
  92. package/src/state/hooks.js +633 -0
  93. package/src/view-config-upgraders.js +628 -0
  94. package/src/view-config-utils.js +315 -0
  95. package/src/view-config-utils.test.fixtures.js +893 -0
  96. package/src/view-config-utils.test.js +95 -0
  97. package/src/view-config-versions.js +68 -0
  98. package/src/vitessce-grid-layout/VitessceGridLayout.js +158 -0
  99. package/src/vitessce-grid-layout/VitessceGridLayout.test.jsx +56 -0
  100. package/{dist → src}/vitessce-grid-layout/index.js +0 -0
  101. package/src/vitessce-grid-layout/layout-utils.js +76 -0
  102. package/src/vitessce-grid-layout/layout-utils.test.js +167 -0
  103. package/src/vitessce-grid-utils.js +160 -0
  104. package/dist/CallbackPublisher.js +0 -68
  105. package/dist/LoadingIndicator.js +0 -27
  106. package/dist/TitleInfo.js +0 -151
  107. package/dist/VitS.js +0 -112
  108. package/dist/VitessceGrid.js +0 -59
  109. package/dist/VitessceGrid.test.js +0 -36
  110. package/dist/Warning.js +0 -45
  111. package/dist/component-registry.js +0 -16
  112. package/dist/data/AbstractLoader.js +0 -35
  113. package/dist/data/LoaderResult.js +0 -7
  114. package/dist/data/loader-registry.js +0 -15
  115. package/dist/data-hook-utils.js +0 -175
  116. package/dist/data-hooks.js +0 -241
  117. package/dist/errors/AbstractLoaderError.js +0 -13
  118. package/dist/errors/DataSourceFetchError.js +0 -13
  119. package/dist/errors/DatasetNotFoundError.js +0 -17
  120. package/dist/errors/LoaderNotFoundError.js +0 -17
  121. package/dist/errors/LoaderValidationError.js +0 -15
  122. package/dist/errors/OptionsValidationError.js +0 -15
  123. package/dist/file-options-schemas-legacy.js +0 -136
  124. package/dist/file-options-schemas-legacy.test.js +0 -126
  125. package/dist/file-options-schemas.js +0 -322
  126. package/dist/file-options-schemas.test.js +0 -23
  127. package/dist/hooks.js +0 -215
  128. package/dist/joint-file-types-legacy.js +0 -278
  129. package/dist/joint-file-types-legacy.test.js +0 -400
  130. package/dist/joint-file-types.js +0 -148
  131. package/dist/joint-file-types.test.js +0 -139
  132. package/dist/plugins.test.fixtures.js +0 -76
  133. package/dist/plugins.test.js +0 -30
  134. package/dist/schemas/config-0.1.0.schema.json +0 -84
  135. package/dist/schemas/config-1.0.0.schema.json +0 -733
  136. package/dist/schemas/config-1.0.1.schema.json +0 -909
  137. package/dist/schemas/config-1.0.10.schema.json +0 -969
  138. package/dist/schemas/config-1.0.11.schema.json +0 -990
  139. package/dist/schemas/config-1.0.12.schema.json +0 -997
  140. package/dist/schemas/config-1.0.13.schema.json +0 -1013
  141. package/dist/schemas/config-1.0.14.schema.json +0 -1048
  142. package/dist/schemas/config-1.0.15.schema.json +0 -1048
  143. package/dist/schemas/config-1.0.15.schema.test.js +0 -22
  144. package/dist/schemas/config-1.0.2.schema.json +0 -910
  145. package/dist/schemas/config-1.0.3.schema.json +0 -910
  146. package/dist/schemas/config-1.0.4.schema.json +0 -949
  147. package/dist/schemas/config-1.0.5.schema.json +0 -949
  148. package/dist/schemas/config-1.0.6.schema.json +0 -949
  149. package/dist/schemas/config-1.0.7.schema.json +0 -949
  150. package/dist/schemas/config-1.0.8.schema.json +0 -965
  151. package/dist/schemas/config-1.0.9.schema.json +0 -965
  152. package/dist/schemas/obsSets.schema.js +0 -195
  153. package/dist/schemas/obsSetsTabular.schema.js +0 -38
  154. package/dist/schemas/raster.schema.js +0 -120
  155. package/dist/shared-mui/components.js +0 -35
  156. package/dist/shared-mui/container.js +0 -211
  157. package/dist/shared-mui/styles.js +0 -73
  158. package/dist/shared-plot-options/CellColorEncodingOption.js +0 -18
  159. package/dist/shared-plot-options/OptionSelect.js +0 -12
  160. package/dist/shared-plot-options/OptionsContainer.js +0 -12
  161. package/dist/shared-plot-options/styles.js +0 -48
  162. package/dist/state/hooks.js +0 -549
  163. package/dist/view-config-upgraders.js +0 -525
  164. package/dist/view-config-utils.js +0 -286
  165. package/dist/view-config-utils.test.fixtures.js +0 -765
  166. package/dist/view-config-utils.test.js +0 -66
  167. package/dist/view-config-versions.js +0 -47
  168. package/dist/vitessce-grid-layout/VitessceGridLayout.js +0 -95
  169. package/dist/vitessce-grid-layout/VitessceGridLayout.test.js +0 -40
  170. package/dist/vitessce-grid-layout/layout-utils.js +0 -60
  171. package/dist/vitessce-grid-layout/layout-utils.test.js +0 -146
  172. package/dist/vitessce-grid-utils.js +0 -140
@@ -0,0 +1,315 @@
1
+ /* eslint-disable no-plusplus */
2
+ /* eslint-disable camelcase */
3
+ import difference from 'lodash/difference';
4
+ import cloneDeep from 'lodash/cloneDeep';
5
+ import { getNextScope } from '@vitessce/utils';
6
+ import {
7
+ AUTO_INDEPENDENT_COORDINATION_TYPES,
8
+ } from '@vitessce/constants-internal';
9
+ import { getViewTypes } from './component-registry';
10
+ import {
11
+ getComponentCoordinationTypes,
12
+ getDefaultCoordinationValues,
13
+ getCoordinationTypes,
14
+ getFileTypes,
15
+ getJointFileTypes,
16
+ } from './plugins';
17
+ import { SCHEMA_HANDLERS } from './view-config-versions';
18
+
19
+ /**
20
+ * Get a list of all unique scope names for a
21
+ * particular coordination type, which exist in
22
+ * a particular view config.
23
+ * @param {object} config A view config object.
24
+ * @param {string} coordinationType A coordination type,
25
+ * for example 'spatialZoom' or 'dataset'.
26
+ * @returns {string[]} Array of existing coordination scope names.
27
+ */
28
+ export function getExistingScopesForCoordinationType(config, coordinationType) {
29
+ const spaceScopes = Object.keys(config?.coordinationSpace?.[coordinationType] || {});
30
+ const componentScopes = config.layout.map(c => c.coordinationScopes?.[coordinationType]);
31
+ return Array.from(new Set([...spaceScopes, ...componentScopes]));
32
+ }
33
+
34
+ /**
35
+ * Give each component the same scope name for this coordination type.
36
+ * @param {object} config A view config object.
37
+ * @param {string} coordinationType A coordination type,
38
+ * for example 'spatialZoom' or 'dataset'.
39
+ * @param {*} scopeValue The initial value for the coordination scope,
40
+ * to set in the coordination space.
41
+ * @returns {object} The new view config.
42
+ */
43
+ function coordinateComponentsTogether(config, coordinationType, scopeValue) {
44
+ const componentCoordinationTypes = getComponentCoordinationTypes();
45
+ const scopeName = getNextScope(getExistingScopesForCoordinationType(config, coordinationType));
46
+ const newConfig = {
47
+ ...config,
48
+ coordinationSpace: {
49
+ ...config.coordinationSpace,
50
+ [coordinationType]: {
51
+ ...config?.coordinationSpace?.[coordinationType],
52
+ // Add the new scope name and value to the coordination space.
53
+ [scopeName]: scopeValue,
54
+ },
55
+ },
56
+ layout: config.layout.map(component => ({
57
+ ...component,
58
+ coordinationScopes: {
59
+ ...component.coordinationScopes,
60
+ // Only set the coordination scope if this component uses this coordination type,
61
+ // and the component is missing a coordination scope for this coordination type.
62
+ ...((
63
+ componentCoordinationTypes[component.component].includes(coordinationType)
64
+ && !component.coordinationScopes?.[coordinationType]
65
+ ) ? {
66
+ // Only set the new scope name if the scope name
67
+ // for this component and coordination type is currently undefined.
68
+ [coordinationType]: scopeName,
69
+ } : {}),
70
+ },
71
+ })),
72
+ };
73
+ return newConfig;
74
+ }
75
+
76
+ /**
77
+ * Give each component a different scope name for this coordination type.
78
+ * @param {object} config A view config object.
79
+ * @param {string} coordinationType A coordination type,
80
+ * for example 'spatialZoom' or 'dataset'.
81
+ * @param {*} scopeValue The initial value for the coordination scope,
82
+ * to set in the coordination space.
83
+ * @returns {object} The new view config.
84
+ */
85
+ function coordinateComponentsIndependent(config, coordinationType, scopeValue) {
86
+ const componentCoordinationTypes = getComponentCoordinationTypes();
87
+ const newConfig = {
88
+ ...config,
89
+ layout: [...config.layout],
90
+ };
91
+ const newScopes = {};
92
+ newConfig.layout.forEach((component, i) => {
93
+ // Only set the coordination scope if this component uses this coordination type,
94
+ // and the component is missing a coordination scope for this coordination type.
95
+ if (componentCoordinationTypes[component.component].includes(coordinationType)
96
+ && !component.coordinationScopes?.[coordinationType]
97
+ ) {
98
+ const scopeName = getNextScope([
99
+ ...getExistingScopesForCoordinationType(config, coordinationType),
100
+ ...Object.keys(newScopes),
101
+ ]);
102
+ newScopes[scopeName] = scopeValue;
103
+ newConfig.layout[i] = {
104
+ ...component,
105
+ coordinationScopes: {
106
+ ...component.coordinationScopes,
107
+ [coordinationType]: scopeName,
108
+ },
109
+ };
110
+ }
111
+ });
112
+ newConfig.coordinationSpace = {
113
+ ...newConfig.coordinationSpace,
114
+ [coordinationType]: {
115
+ ...newConfig.coordinationSpace[coordinationType],
116
+ // Add the new scope name and value to the coordination space.
117
+ ...newScopes,
118
+ },
119
+ };
120
+ return newConfig;
121
+ }
122
+
123
+ function initializeAuto(config) {
124
+ let newConfig = config;
125
+ const { layout, datasets } = newConfig;
126
+
127
+ const componentCoordinationTypes = getComponentCoordinationTypes();
128
+ const defaultCoordinationValues = getDefaultCoordinationValues();
129
+ const coordinationTypes = getCoordinationTypes();
130
+
131
+ // For each coordination type, check whether it requires initialization.
132
+ coordinationTypes.forEach((coordinationType) => {
133
+ // A coordination type requires coordination if at least one component is missing
134
+ // a (coordination type, coordination scope) tuple.
135
+ // Components may only use a subset of all coordination types.
136
+ const requiresCoordination = !layout
137
+ .every(c => (
138
+ (!componentCoordinationTypes[c.component].includes(coordinationType))
139
+ || c.coordinationScopes?.[coordinationType]
140
+ ));
141
+ if (requiresCoordination) {
142
+ // Note that the default value may be undefined.
143
+ let defaultValue = defaultCoordinationValues[coordinationType];
144
+ // Check whether this is the special 'dataset' coordination type.
145
+ if (coordinationType === 'dataset' && datasets.length >= 1) {
146
+ // Use the first dataset ID as the default
147
+ // if there is at least one dataset.
148
+ defaultValue = datasets[0].uid;
149
+ }
150
+ // Use the list of "independent" coordination types
151
+ // to determine whether a particular coordination type
152
+ // should be initialized to
153
+ // a unique scope for every component ("independent")
154
+ // vs. the same scope for every component ("together").
155
+ if (AUTO_INDEPENDENT_COORDINATION_TYPES.includes(coordinationType)) {
156
+ newConfig = coordinateComponentsIndependent(newConfig, coordinationType, defaultValue);
157
+ } else {
158
+ newConfig = coordinateComponentsTogether(newConfig, coordinationType, defaultValue);
159
+ }
160
+ }
161
+ });
162
+
163
+ return newConfig;
164
+ }
165
+
166
+ export function checkTypes(config) {
167
+ // Add a log message when there are additionalProperties in the coordination space that
168
+ // do not appear in the view config JSON schema,
169
+ // with a note that this indicates either a mistake or custom coordination type usage.
170
+ const coordinationTypesInConfig = Object.keys(config.coordinationSpace || {});
171
+ const allCoordinationTypes = getCoordinationTypes();
172
+ const unknownCoordinationTypes = difference(coordinationTypesInConfig, allCoordinationTypes);
173
+ if (unknownCoordinationTypes.length > 0) {
174
+ return [false, `The following coordination types are not recognized: [${unknownCoordinationTypes}].\nIf these are plugin coordination types, ensure that they have been properly registered.`];
175
+ }
176
+ // Add a log message when there are views in the layout that are neither
177
+ // core views nor registered plugin views.
178
+ const viewTypesInConfig = config.layout.map(c => c.component);
179
+ const allViewTypes = getViewTypes();
180
+ const unknownViewTypes = difference(viewTypesInConfig, allViewTypes);
181
+ if (unknownViewTypes.length > 0) {
182
+ return [false, `The following view types are not recognized: [${unknownViewTypes}].\nIf these are plugin view types, ensure that they have been properly registered.`];
183
+ }
184
+ // Add a log message when there are file definitions with neither
185
+ // core nor registered plugin file types.
186
+ const fileTypesInConfig = config.datasets.flatMap(d => d.files.map(f => f.fileType));
187
+ const allFileTypes = getFileTypes();
188
+ const unknownFileTypes = difference(fileTypesInConfig, allFileTypes);
189
+ if (unknownFileTypes.length > 0) {
190
+ return [false, `The following file types are not recognized: [${unknownFileTypes}].\nIf these are plugin file types, ensure that they have been properly registered.`];
191
+ }
192
+ return [true, 'All view types, coordination types, and file types that appear in the view config are recognized.'];
193
+ }
194
+
195
+ /**
196
+ * Assign unique ids for view definitions where
197
+ * they are missing a value for the uid property
198
+ * in layout[].uid.
199
+ * @param {object} config The view config
200
+ * @returns The updated view config.
201
+ */
202
+ function assignViewUids(config) {
203
+ const { layout } = config;
204
+ const usedIds = layout.map(view => view.uid);
205
+ layout.forEach((view, i) => {
206
+ // Assign uids for views where they are not present.
207
+ if (!view.uid) {
208
+ const nextUid = getNextScope(usedIds);
209
+ layout[i].uid = nextUid;
210
+ usedIds.push(nextUid);
211
+ }
212
+ });
213
+ return {
214
+ ...config,
215
+ layout,
216
+ };
217
+ }
218
+
219
+ /**
220
+ * Expand convenience file definitions. Each convenience file
221
+ * definition expansion function takes in one file definition and
222
+ * returns an array of file definitions. Not performed recursively.
223
+ * @param {object} config The view config containing collapsed
224
+ * convenience file types.
225
+ * @returns The view config containing expanded minimal file types.
226
+ */
227
+ function expandConvenienceFileDefs(config) {
228
+ const convenienceFileTypes = getJointFileTypes();
229
+ const { datasets: currDatasets } = config;
230
+ const datasets = cloneDeep(currDatasets);
231
+ currDatasets.forEach((dataset, i) => {
232
+ const { files = [] } = dataset;
233
+ let newFiles = [];
234
+ files.forEach((fileDef) => {
235
+ const { fileType } = fileDef;
236
+ const expansionFunc = convenienceFileTypes[fileType];
237
+ if (expansionFunc && typeof expansionFunc === 'function') {
238
+ // This was a convenience file type, so expand it.
239
+ const expandedFileDefs = expansionFunc(fileDef);
240
+ newFiles = newFiles.concat(expandedFileDefs);
241
+ } else {
242
+ // This was not a convenience file type,
243
+ // so keep it in the files array as-is.
244
+ newFiles.push(fileDef);
245
+ }
246
+ });
247
+ datasets[i].files = newFiles;
248
+ });
249
+ return {
250
+ ...config,
251
+ datasets,
252
+ };
253
+ }
254
+
255
+ /**
256
+ * Initialize the view config:
257
+ * - Fill in missing coordination objects with default values.
258
+ * - Fill in missing component coordination scope mappings.
259
+ * based on the `initStrategy` specified in the view config.
260
+ * - Fill in missing view uid values.
261
+ * - Expand convenience file types.
262
+ * Should be "stable": if run on the same view config twice, the return value the second
263
+ * time should be identical to the return value the first time.
264
+ * @param {object} config The view config prop.
265
+ * @returns The initialized view config.
266
+ */
267
+ export function initialize(config) {
268
+ let newConfig = cloneDeep(config);
269
+ if (newConfig.initStrategy === 'auto') {
270
+ newConfig = initializeAuto(config);
271
+ }
272
+ newConfig = expandConvenienceFileDefs(newConfig);
273
+ return assignViewUids(newConfig);
274
+ }
275
+
276
+ export function upgradeAndValidate(oldConfig, onConfigUpgrade = null) {
277
+ // oldConfig object must have a `version` property.
278
+ let nextConfig = oldConfig;
279
+ let fromVersion;
280
+ let upgradeFunction; let
281
+ validateFunction;
282
+
283
+ do {
284
+ fromVersion = nextConfig.version;
285
+
286
+ if (!Object.keys(SCHEMA_HANDLERS).includes(fromVersion)) {
287
+ return [{
288
+ title: 'Config validation failed',
289
+ preformatted: 'Unknown config version.',
290
+ }, false];
291
+ }
292
+
293
+ [validateFunction, upgradeFunction] = SCHEMA_HANDLERS[fromVersion];
294
+
295
+ // Validate under the legacy schema before upgrading.
296
+ const validLegacy = validateFunction(nextConfig);
297
+ if (!validLegacy) {
298
+ const failureReason = JSON.stringify(validateFunction.errors, null, 2);
299
+ return [{
300
+ title: 'Config validation failed',
301
+ preformatted: failureReason,
302
+ }, false];
303
+ }
304
+
305
+ if (upgradeFunction) {
306
+ const prevConfig = nextConfig;
307
+ nextConfig = upgradeFunction(nextConfig);
308
+ if (onConfigUpgrade) {
309
+ onConfigUpgrade(prevConfig, nextConfig);
310
+ }
311
+ }
312
+ } while (upgradeFunction);
313
+
314
+ return [nextConfig, true];
315
+ }