@aics/vole-app 2.13.7 → 2.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 (23) hide show
  1. package/es/Globals.d.js +0 -0
  2. package/es/aics-image-viewer/components/App/index.js +43 -8
  3. package/es/aics-image-viewer/components/ChannelsWidgetRow/index.js +38 -20
  4. package/es/aics-image-viewer/components/GlobalVolumeControls.js +3 -2
  5. package/es/aics-image-viewer/components/ViewerStateProvider/ResetStateProvider.js +4 -1
  6. package/es/aics-image-viewer/components/shared/SliderRow/index.js +8 -3
  7. package/es/aics-image-viewer/components/shared/SliderRow/styles.css +6 -0
  8. package/es/aics-image-viewer/shared/utils/datatypes.js +29 -0
  9. package/es/aics-image-viewer/shared/utils/firebase/index.js +109 -0
  10. package/es/aics-image-viewer/shared/utils/math.js +3 -0
  11. package/es/aics-image-viewer/shared/utils/test/urlParsing.test.js +1112 -0
  12. package/es/aics-image-viewer/shared/utils/urlParsing.js +1255 -0
  13. package/es/index.js +2 -1
  14. package/package.json +2 -2
  15. package/type-declarations/aics-image-viewer/components/App/types.d.ts +1 -0
  16. package/type-declarations/aics-image-viewer/components/Toolbar/index.d.ts +1 -1
  17. package/type-declarations/aics-image-viewer/components/ViewerStateProvider/index.d.ts +1 -1
  18. package/type-declarations/aics-image-viewer/components/shared/SliderRow/index.d.ts +2 -1
  19. package/type-declarations/aics-image-viewer/shared/utils/datatypes.d.ts +10 -0
  20. package/type-declarations/aics-image-viewer/shared/utils/firebase/index.d.ts +69 -0
  21. package/type-declarations/aics-image-viewer/shared/utils/math.d.ts +1 -0
  22. package/type-declarations/aics-image-viewer/shared/utils/urlParsing.d.ts +344 -0
  23. package/type-declarations/index.d.ts +3 -1
File without changes
@@ -26,6 +26,7 @@ import { colorArrayToFloats } from "../../shared/utils/colorRepresentations";
26
26
  import { controlPointsToRamp, initializeLut, rampToControlPoints, remapControlPointsForChannel } from "../../shared/utils/controlPointsToLut";
27
27
  import { useConstructor } from "../../shared/utils/hooks";
28
28
  import { alphaSliderToImageValue, brightnessSliderToImageValue, densitySliderToImageValue, gammaSliderToImageValues } from "../../shared/utils/sliderValuesToImageValues";
29
+ import { findFirstChannelMatch } from "../../shared/utils/viewerChannelSettings";
29
30
  import useVolume, { ImageLoadStatus } from "../useVolume";
30
31
  import CellViewerCanvasWrapper from "../CellViewerCanvasWrapper";
31
32
  import ControlPanel from "../ControlPanel";
@@ -122,6 +123,7 @@ var App = function App(props) {
122
123
  onResetChannel = _viewerState$current.onResetChannel;
123
124
  var _props = props,
124
125
  onControlPanelToggle = _props.onControlPanelToggle,
126
+ onImageTitleChange = _props.onImageTitleChange,
125
127
  metadata = _props.metadata,
126
128
  metadataFormatter = _props.metadataFormatter;
127
129
  useMemo(function () {
@@ -184,14 +186,41 @@ var App = function App(props) {
184
186
  // we need to keep track of channel ranges for remapping control points
185
187
  var channelRangesRef = useRef([]);
186
188
  var onCreateImage = useCallback(function (newImage) {
189
+ var _newImage$imageInfo$c;
187
190
  if (newImage === null) {
188
191
  return;
189
192
  }
190
- channelRangesRef.current = new Array(newImage.channelNames.length).fill(undefined);
193
+ var channelNames = newImage.channelNames;
194
+ channelRangesRef.current = new Array(channelNames.length).fill(undefined);
191
195
  var channelSettings = viewerState.current.channelSettings;
196
+
197
+ // If the image has channel color metadata, apply those colors now
198
+ var viewerChannelSettings = getCurrentViewerChannelSettings();
199
+ var channelColorMeta = (_newImage$imageInfo$c = newImage.imageInfo.channelColors) === null || _newImage$imageInfo$c === void 0 ? void 0 : _newImage$imageInfo$c.map(function (color, index) {
200
+ // Filter out channels that have colors in `viewerChannelSettings`
201
+ if (viewerChannelSettings === undefined) {
202
+ return color;
203
+ }
204
+ var settings = findFirstChannelMatch(channelNames[index], index, viewerChannelSettings);
205
+ if ((settings === null || settings === void 0 ? void 0 : settings.color) !== undefined) {
206
+ return undefined;
207
+ } else {
208
+ return color;
209
+ }
210
+ });
211
+ if (Array.isArray(channelColorMeta)) {
212
+ channelColorMeta.forEach(function (color, index) {
213
+ if (Array.isArray(color) && index < channelNames.length) {
214
+ changeChannelSetting(index, {
215
+ color: color
216
+ });
217
+ }
218
+ });
219
+ }
192
220
  view3d.addVolume(newImage, {
193
221
  // Immediately passing down channel parameters isn't strictly necessary, but keeps things looking consistent on load
194
- channels: newImage.channelNames.map(function (name) {
222
+ channels: newImage.channelNames.map(function (name, index) {
223
+ var _channelColorMeta$ind;
195
224
  // TODO do we really need to be searching by name here?
196
225
  var ch = channelSettings.find(function (channel) {
197
226
  return channel.name === name;
@@ -204,12 +233,13 @@ var App = function App(props) {
204
233
  isosurfaceEnabled: ch.isosurfaceEnabled,
205
234
  isovalue: ch.isovalue,
206
235
  isosurfaceOpacity: ch.opacity,
207
- color: ch.color
236
+ color: (_channelColorMeta$ind = channelColorMeta === null || channelColorMeta === void 0 ? void 0 : channelColorMeta[index]) !== null && _channelColorMeta$ind !== void 0 ? _channelColorMeta$ind : ch.color
208
237
  };
209
238
  })
210
239
  });
240
+ onImageTitleChange === null || onImageTitleChange === void 0 || onImageTitleChange(newImage.imageInfo.imageInfo.name);
211
241
  view3d.updateActiveChannels(newImage);
212
- }, [view3d, viewerState]);
242
+ }, [view3d, viewerState, onImageTitleChange, changeChannelSetting, getCurrentViewerChannelSettings]);
213
243
  var onChannelLoaded = useCallback(function (image, channelIndex, isInitialLoad) {
214
244
  // TODO this was once a search by name - is that still necessary or will the index always be correct?
215
245
  var thisChannelSettings = channelSettings[channelIndex];
@@ -224,13 +254,14 @@ var App = function App(props) {
224
254
  var _initializeLut = initializeLut(image, channelIndex, getCurrentViewerChannelSettings()),
225
255
  ramp = _initializeLut.ramp,
226
256
  controlPoints = _initializeLut.controlPoints;
227
- var dtype = thisChannel.dtype;
257
+ var range = DTYPE_RANGE[thisChannel.dtype];
228
258
  changeChannelSetting(channelIndex, {
229
259
  controlPoints: controlPoints,
230
260
  ramp: controlPointsToRamp(ramp),
231
261
  // set the default range of the transfer function editor to cover the full range of the data type
232
- plotMin: DTYPE_RANGE[dtype].min,
233
- plotMax: DTYPE_RANGE[dtype].max
262
+ plotMin: range.min,
263
+ plotMax: range.max,
264
+ isovalue: range.min + (range.max - range.min) / 2
234
265
  });
235
266
  } else {
236
267
  // This channel has already been initialized, but its LUT was just remapped and we need to update some things
@@ -268,10 +299,14 @@ var App = function App(props) {
268
299
  view3d.updateActiveChannels(image);
269
300
  }
270
301
  }, [view3d, channelSettings, maskChannelName, viewerState]);
302
+ var onError = useCallback(function (error) {
303
+ showError(error);
304
+ onImageTitleChange === null || onImageTitleChange === void 0 || onImageTitleChange(undefined);
305
+ }, [showError, onImageTitleChange]);
271
306
  var volume = useVolume(scenes, {
272
307
  onCreateImage: onCreateImage,
273
308
  onChannelLoaded: onChannelLoaded,
274
- onError: showError,
309
+ onError: onError,
275
310
  maskChannelName: maskChannelName
276
311
  });
277
312
  var image = volume.image,
@@ -4,9 +4,9 @@ function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r)
4
4
  function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
5
5
  function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t["return"] && (u = t["return"](), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
6
6
  function _arrayWithHoles(r) { if (Array.isArray(r)) return r; }
7
- import { Button, Checkbox, List } from "antd";
7
+ import { Button, Checkbox, InputNumber, List } from "antd";
8
8
  import React, { useCallback, useState } from "react";
9
- import { ISOSURFACE_OPACITY_SLIDER_MAX } from "../../shared/constants";
9
+ import { DTYPE_RANGE, ISOSURFACE_OPACITY_SLIDER_MAX } from "../../shared/constants";
10
10
  import { colorArrayToObject, colorObjectToArray } from "../../shared/utils/colorRepresentations";
11
11
  import ColorPicker from "../ColorPicker";
12
12
  import SliderRow from "../shared/SliderRow";
@@ -37,20 +37,6 @@ var ChannelsWidgetRow = function ChannelsWidgetRow(props) {
37
37
  isosurfaceEnabled: target.checked
38
38
  });
39
39
  };
40
- var onIsovalueChange = function onIsovalueChange(_ref3) {
41
- var _ref4 = _slicedToArray(_ref3, 1),
42
- newValue = _ref4[0];
43
- return changeSettingForThisChannel({
44
- isovalue: newValue
45
- });
46
- };
47
- var onOpacityChange = function onOpacityChange(_ref5) {
48
- var _ref6 = _slicedToArray(_ref5, 1),
49
- newValue = _ref6[0];
50
- return changeSettingForThisChannel({
51
- opacity: newValue / ISOSURFACE_OPACITY_SLIDER_MAX
52
- });
53
- };
54
40
  var onColorChange = function onColorChange(newRGB, _oldRGB, index) {
55
41
  var color = colorObjectToArray(newRGB);
56
42
  props.changeChannelSetting(index, {
@@ -112,17 +98,49 @@ var ChannelsWidgetRow = function ChannelsWidgetRow(props) {
112
98
  });
113
99
  };
114
100
  var renderSurfaceControls = function renderSurfaceControls() {
101
+ var range = DTYPE_RANGE[props.channelDataForChannel.dtype];
115
102
  return /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement(SliderRow, {
116
103
  label: "Isovalue",
117
- max: 255,
104
+ min: range.min,
105
+ max: range.max,
118
106
  start: channelState.isovalue,
119
- onChange: onIsovalueChange,
107
+ onChange: function onChange(_ref3) {
108
+ var _ref4 = _slicedToArray(_ref3, 1),
109
+ isovalue = _ref4[0];
110
+ return changeSettingForThisChannel({
111
+ isovalue: isovalue
112
+ });
113
+ },
120
114
  formatInteger: true
121
- }), /*#__PURE__*/React.createElement(SliderRow, {
115
+ }, /*#__PURE__*/React.createElement(InputNumber, {
116
+ value: channelState.isovalue,
117
+ onChange: function onChange(isovalue) {
118
+ return isovalue !== null && changeSettingForThisChannel({
119
+ isovalue: isovalue
120
+ });
121
+ },
122
+ formatter: function formatter(v) {
123
+ return v === undefined ? "" : Number(v).toFixed(0);
124
+ },
125
+ min: range.min,
126
+ max: range.max,
127
+ size: "small",
128
+ controls: false,
129
+ style: {
130
+ width: "64px",
131
+ marginLeft: "8px"
132
+ }
133
+ })), /*#__PURE__*/React.createElement(SliderRow, {
122
134
  label: "Opacity",
123
135
  max: ISOSURFACE_OPACITY_SLIDER_MAX,
124
136
  start: channelState.opacity * ISOSURFACE_OPACITY_SLIDER_MAX,
125
- onChange: onOpacityChange,
137
+ onChange: function onChange(_ref5) {
138
+ var _ref6 = _slicedToArray(_ref5, 1),
139
+ opacity = _ref6[0];
140
+ return changeSettingForThisChannel({
141
+ opacity: opacity / ISOSURFACE_OPACITY_SLIDER_MAX
142
+ });
143
+ },
126
144
  formatInteger: true
127
145
  }), /*#__PURE__*/React.createElement("div", {
128
146
  className: "button-row"
@@ -1,5 +1,5 @@
1
- import React from "react";
2
1
  import { Checkbox } from "antd";
2
+ import React from "react";
3
3
  import SliderRow from "./shared/SliderRow";
4
4
  import { connectToViewerState } from "./ViewerStateProvider";
5
5
  var GlobalVolumeControls = function GlobalVolumeControls(props) {
@@ -26,7 +26,8 @@ var GlobalVolumeControls = function GlobalVolumeControls(props) {
26
26
  paddingBottom: 22
27
27
  }
28
28
  }, showControls.alphaMaskSlider && createSliderRow("mask cell", maskAlpha, 100, "maskAlpha"), showControls.brightnessSlider && createSliderRow("brightness", brightness, 100, "brightness"), showControls.densitySlider && createSliderRow("density", density, 100, "density"), showControls.levelsSliders && createSliderRow("levels", levels, 255, "levels"), showControls.interpolationControl && /*#__PURE__*/React.createElement(SliderRow, {
29
- label: "interpolate"
29
+ label: "interpolate",
30
+ hideSlider: true
30
31
  }, /*#__PURE__*/React.createElement(Checkbox, {
31
32
  checked: props.interpolationEnabled,
32
33
  onChange: function onChange(_ref) {
@@ -132,7 +132,10 @@ var ResetStateProvider = /*#__PURE__*/function () {
132
132
  }, this.savedViewerState);
133
133
  var newChannelSettings = channelSettings.map(function (_, index) {
134
134
  var initialChannelSetting = initializeOneChannelSetting(channelSettings[index].name, index, getDefaultChannelColor(index), _this.savedViewerChannelSettings);
135
- return initialChannelSetting;
135
+ return _objectSpread(_objectSpread({}, initialChannelSetting), {}, {
136
+ plotMin: channelSettings[index].plotMin,
137
+ plotMax: channelSettings[index].plotMax
138
+ });
136
139
  });
137
140
  this.resetToState(newViewerState, newChannelSettings);
138
141
  this.useDefaultViewerChannelSettings = false;
@@ -8,15 +8,18 @@ var INTEGER_FORMATTER = {
8
8
 
9
9
  /** A component to ensure a single unified style across the many labeled slider rows in the control panel */
10
10
  var SliderRow = function SliderRow(props) {
11
+ var _props$min;
11
12
  return /*#__PURE__*/React.createElement("div", {
12
13
  className: "viewer-control-row"
13
14
  }, /*#__PURE__*/React.createElement("div", {
14
15
  className: "control-name"
15
16
  }, props.label), /*#__PURE__*/React.createElement("div", {
16
17
  className: "control"
17
- }, props.start === undefined ? props.children : !props.hideSlider && /*#__PURE__*/React.createElement(SmarterSlider, {
18
+ }, props.start !== undefined && !props.hideSlider && /*#__PURE__*/React.createElement("div", {
19
+ className: "control-slider"
20
+ }, /*#__PURE__*/React.createElement(SmarterSlider, {
18
21
  range: {
19
- min: 0,
22
+ min: (_props$min = props.min) !== null && _props$min !== void 0 ? _props$min : 0,
20
23
  max: props.max
21
24
  },
22
25
  start: props.start,
@@ -26,6 +29,8 @@ var SliderRow = function SliderRow(props) {
26
29
  format: props.formatInteger ? INTEGER_FORMATTER : undefined,
27
30
  onUpdate: props.onUpdate,
28
31
  onChange: props.onChange
29
- })));
32
+ })), props.children && /*#__PURE__*/React.createElement("div", {
33
+ className: "control-extra"
34
+ }, props.children)));
30
35
  };
31
36
  export default SliderRow;
@@ -16,4 +16,10 @@
16
16
 
17
17
  .viewer-control-row .control{
18
18
  flex:5;
19
+ display:flex;
20
+ align-items:center;
19
21
  }
22
+
23
+ .viewer-control-row .control .control-slider{
24
+ flex-grow:1;
25
+ }
@@ -0,0 +1,29 @@
1
+ import { isEqual } from "lodash";
2
+
3
+ /**
4
+ * Returns a (shallow) copy of an object with all properties that are
5
+ * `undefined` removed.
6
+ */
7
+ export function removeUndefinedProperties(obj) {
8
+ var result = {};
9
+ for (var key in obj) {
10
+ if (obj[key] !== undefined) {
11
+ result[key] = obj[key];
12
+ }
13
+ }
14
+ return result;
15
+ }
16
+
17
+ /**
18
+ * Returns a copy of `obj` where all properties with values that match the properties of `match`
19
+ * are removed. Matching is determined by deep equality (see `isDeepEqual()`).
20
+ */
21
+ export function removeMatchingProperties(obj, match) {
22
+ var result = {};
23
+ for (var key in obj) {
24
+ if (!isEqual(obj[key], match[key])) {
25
+ result[key] = obj[key];
26
+ }
27
+ }
28
+ return result;
29
+ }
@@ -0,0 +1,109 @@
1
+ function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
2
+ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
3
+ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
4
+ function _defineProperties(e, r) { for (var t = 0; t < r.length; t++) { var o = r[t]; o.enumerable = o.enumerable || !1, o.configurable = !0, "value" in o && (o.writable = !0), Object.defineProperty(e, _toPropertyKey(o.key), o); } }
5
+ function _createClass(e, r, t) { return r && _defineProperties(e.prototype, r), t && _defineProperties(e, t), Object.defineProperty(e, "prototype", { writable: !1 }), e; }
6
+ function _classCallCheck(a, n) { if (!(a instanceof n)) throw new TypeError("Cannot call a class as a function"); }
7
+ function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
8
+ function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; }
9
+ function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
10
+ // TODO: These types are shared with Cell Feature Explorer. Can they be moved to
11
+ // a shared package?
12
+
13
+ function isDevOrStagingSite(host) {
14
+ // first condition is for testing with no client
15
+ return !host || host.includes("localhost") || host.includes("staging") || host.includes("stg");
16
+ }
17
+ var FirebaseRequest = /*#__PURE__*/_createClass(
18
+ // TODO: These properties are private and unused. Remove?
19
+
20
+ function FirebaseRequest(firestore) {
21
+ var _this = this;
22
+ _classCallCheck(this, FirebaseRequest);
23
+ _defineProperty(this, "getDoc", function (docPath) {
24
+ return _this.firestore.doc(docPath).get();
25
+ });
26
+ _defineProperty(this, "getAvailableDatasets", function () {
27
+ return _this.firestore.collection("dataset-descriptions").get().then(function (snapShot) {
28
+ var datasets = [];
29
+ snapShot.forEach(function (doc) {
30
+ var metadata = doc.data();
31
+ /** if running the site in a local development env or on staging.cfe.allencell.org
32
+ * include all cards, otherwise, only include cards with a production flag.
33
+ * this is based on hostname instead of a build time variable so we don't
34
+ * need a separate build for staging and production
35
+ */
36
+
37
+ if (isDevOrStagingSite(location.hostname)) {
38
+ datasets.push(metadata);
39
+ } else if (metadata.production) {
40
+ datasets.push(metadata);
41
+ }
42
+ });
43
+ return datasets;
44
+ });
45
+ });
46
+ _defineProperty(this, "setCollectionRef", function (id) {
47
+ _this.collectionRef = _this.firestore.collection("cfe-datasets").doc(id);
48
+ });
49
+ _defineProperty(this, "getManifest", function (ref) {
50
+ return _this.firestore.doc(ref).get().then(function (manifestDoc) {
51
+ return manifestDoc.data();
52
+ });
53
+ });
54
+ _defineProperty(this, "selectDataset", function (ref) {
55
+ return _this.getManifest(ref).then(function (data) {
56
+ _this.featuresDataPath = data.featuresDataPath;
57
+ _this.thumbnailRoot = data.thumbnailRoot;
58
+ _this.downloadRoot = data.downloadRoot;
59
+ _this.volumeViewerDataRoot = data.volumeViewerDataRoot;
60
+ _this.featuresDisplayOrder = data.featuresDisplayOrder;
61
+ _this.cellLineDataPath = data.cellLineDataPath;
62
+ _this.fileInfoPath = data.fileInfoPath;
63
+ _this.featuresDataOrder = data.featuresDataOrder;
64
+ _this.featureDefsPath = data.featureDefsPath;
65
+ _this.albumPath = data.albumPath;
66
+ return _objectSpread({}, data);
67
+ });
68
+ });
69
+ _defineProperty(this, "getFileInfoByCellId", function (cellId) {
70
+ return _this.getDoc("".concat(_this.fileInfoPath, "/").concat(cellId)).then(function (doc) {
71
+ var data = doc.data();
72
+ if (!data) {
73
+ return;
74
+ }
75
+ return _objectSpread(_objectSpread({}, data), {}, {
76
+ CellId: data.CellId.toString(),
77
+ FOVId: data.FOVId.toString()
78
+ });
79
+ });
80
+ });
81
+ _defineProperty(this, "getFileInfoByArrayOfCellIds", function (cellIds) {
82
+ return Promise.all(cellIds.map(function (id) {
83
+ return _this.getDoc("".concat(_this.fileInfoPath, "/").concat(id)).then(function (doc) {
84
+ var data = doc.data();
85
+ if (!data) {
86
+ return;
87
+ }
88
+ return _objectSpread(_objectSpread({}, data), {}, {
89
+ CellId: data.CellId.toString(),
90
+ FOVId: data.FOVId.toString()
91
+ });
92
+ });
93
+ }));
94
+ });
95
+ this.firestore = firestore;
96
+ this.featuresDataPath = "";
97
+ this.cellLineDataPath = "";
98
+ this.thumbnailRoot = "";
99
+ this.downloadRoot = "";
100
+ this.volumeViewerDataRoot = "";
101
+ this.featuresDisplayOrder = [];
102
+ this.fileInfoPath = "";
103
+ this.datasetId = "";
104
+ this.featuresDataOrder = [];
105
+ this.albumPath = "";
106
+ this.featureDefsPath = "";
107
+ this.collectionRef = firestore.collection("cfe-datasets").doc("v1");
108
+ });
109
+ export default FirebaseRequest;
@@ -0,0 +1,3 @@
1
+ export function clamp(value, min, max) {
2
+ return Math.min(Math.max(value, min), max);
3
+ }