From 4b1dd13871419e19e8dc6ad4e1ce1b0ca5fc482d Mon Sep 17 00:00:00 2001 From: Anne Haley Date: Wed, 4 Sep 2024 15:31:04 +0000 Subject: [PATCH 001/103] Add basic APIs for specifying frame values, add test --- .../zarr/large_image_source_zarr/__init__.py | 97 +++++++++++++++- test/test_sink.py | 106 ++++++++++++++++++ 2 files changed, 200 insertions(+), 3 deletions(-) diff --git a/sources/zarr/large_image_source_zarr/__init__.py b/sources/zarr/large_image_source_zarr/__init__.py index 1296f1287..54dd569bf 100644 --- a/sources/zarr/large_image_source_zarr/__init__.py +++ b/sources/zarr/large_image_source_zarr/__init__.py @@ -98,6 +98,9 @@ def _initOpen(self, **kwargs): self._largeImagePath = str(self._getLargeImagePath()) self._zarr = None self._editable = False + self._frameValues = None + self._frameAxes = None + self._frameUnits = None if not os.path.isfile(self._largeImagePath) and '//:' not in self._largeImagePath: raise TileSourceFileNotFoundError(self._largeImagePath) from None try: @@ -152,6 +155,9 @@ def _initNew(self, path, **kwargs): self._imageDescription = None self._levels = [] self._associatedImages = {} + self._frameValues = None + self._frameAxes = None + self._frameUnits = None def __del__(self): if not hasattr(self, '_derivedSource'): @@ -386,8 +392,10 @@ def _validateZarr(self): stride = 1 self._strides = {} self._axisCounts = {} - for _, k in sorted((-'tzc'.index(k) if k in 'tzc' else 1, k) - for k in self._axes if k not in 'xys'): + for _, k in sorted( + (-self._axes.get(k, 'tzc'.index(k) if k in 'tzc' else -1), k) + for k in self._axes if k not in 'xys' + ): self._strides[k] = stride self._axisCounts[k] = baseArray.shape[self._axes[k]] stride *= baseArray.shape[self._axes[k]] @@ -442,11 +450,32 @@ def getMetadata(self): result = super().getMetadata() if self._framecount > 1: result['frames'] = frames = [] + if self.frameValues is not None and self.frameAxes is not None: + for i, axis in enumerate(self.frameAxes): + all_frame_values = self.frameValues[..., i] + split = np.split(all_frame_values, all_frame_values.shape[i], axis=i) + values = [a.flat[0] for a in split] + uniform = all(len(np.unique(a)) == 1 for a in split) + result['Value' + axis.upper()] = dict( + values=values, + uniform=uniform, + units=self.frameUnits.get(axis) if self.frameUnits is not None else None, + min=min(values), + max=max(values), + datatype=np.array(values).dtype.name + ) for idx in range(self._framecount): frame = {'Frame': idx} for axis in self._strides: frame['Index' + axis.upper()] = ( idx // self._strides[axis]) % self._axisCounts[axis] + if self.frameValues is not None and self.frameAxes is not None: + current_frame_slice = tuple( + frame['Index' + axis.upper()] for axis in self.frameAxes + ) + current_frame_values = self.frameValues[current_frame_slice] + for i, axis in enumerate(self.frameAxes): + frame['Value' + axis.upper()] = current_frame_values[i] frames.append(frame) self._addMetadataFrameInformation(result, getattr(self, '_channels', None)) return result @@ -596,7 +625,11 @@ def addTile(self, tile, x=0, y=0, mask=None, axes=None, **kwargs): placement = { 'x': x, 'y': y, - **kwargs, + **{k: v for k, v in kwargs.items() if not k.endswith('_value')}, + } + frame_values = { + k.replace('_value', ''): v for k, v in kwargs.items() + if k not in placement } tile, mask, placement, axes = self._validateNewTile(tile, mask, placement, axes) @@ -615,6 +648,21 @@ def addTile(self, tile, x=0, y=0, mask=None, axes=None, **kwargs): for i, a in enumerate(axes) ]) + if len(frame_values.keys()) > 0: + self.frameAxes = [a for a in axes if a in frame_values] + frames_shape = [new_dims[a] for a in self.frameAxes] + frames_shape.append(len(frames_shape)) + if self.frameValues is None: + self.frameValues = np.empty(frames_shape, dtype=object) + elif self.frameValues.shape != frames_shape: + self.frameValues = np.pad( + self.frameValues, + [(0, s - self.frameValues.shape[i]) for i, s in enumerate(frames_shape)] + ) + current_frame_slice = tuple(placement.get(a) for a in self.frameAxes) + for i, k in enumerate(self.frameAxes): + self.frameValues[(*current_frame_slice, i)] = frame_values.get(k) + current_arrays = dict(self._zarr.arrays()) if store_path == '0': # if writing to base data, invalidate generated levels @@ -725,6 +773,9 @@ def _writeInternalMetadata(self): rdefs['defaultT'] = 0 elif a == 'z': rdefs['defaultZ'] = 0 + unit = self.frameUnits.get(a) if self.frameUnits is not None else None + if unit is not None: + axis_metadata['unit'] = unit axes.append(axis_metadata) if channel_axis is not None and len(arrays) > 0: base_array = list(arrays.values())[0] @@ -860,6 +911,46 @@ def channelColors(self, colors): self._checkEditable() self._channelColors = colors + @property + def frameAxes(self): + return self._frameAxes + + @frameAxes.setter + def frameAxes(self, axes): + self._checkEditable() + self._frameAxes = axes + + @property + def frameUnits(self): + return self._frameUnits + + @frameUnits.setter + def frameUnits(self, units): + self._checkEditable() + if self.frameAxes is None: + err = 'frameAxes must be set first with a list of frame axis names.' + raise ValueError + if not isinstance(units, dict) or not all( + k in self.frameAxes for k in units.keys() + ): + err = 'frameUnits must be a dictionary with keys that exist in frameAxes.' + self._frameUnits = units + + @property + def frameValues(self): + return self._frameValues + + @frameValues.setter + def frameValues(self, a): + self._checkEditable() + if self.frameAxes is None: + err = 'frameAxes must be set first with a list of frame axis names.' + raise ValueError + if len(a.shape) != len(self.frameAxes) + 1: + err = f'frameValues must have {len(self.frameAxes) + 1} dimensions.' + raise ValueError(err) + self._frameValues = a + def _generateDownsampledLevels(self, resample_method): self._checkEditable() current_arrays = dict(self._zarr.arrays()) diff --git a/test/test_sink.py b/test/test_sink.py index df6e7c6ca..3bd5dd74b 100644 --- a/test/test_sink.py +++ b/test/test_sink.py @@ -535,3 +535,109 @@ def testConcurrency(tmp_path): assert data is not None assert data.shape == seed_data.shape assert np.allclose(data, seed_data) + + +def compare_metadata(actual, expected): + assert type(actual) is type(expected) + if isinstance(actual, list): + for i, v in enumerate(actual): + compare_metadata(v, expected[i]) + elif isinstance(actual, dict): + assert len(actual.keys()) == len(expected.keys()) + for k, v in actual.items(): + compare_metadata(v, expected[k]) + else: + assert actual == expected + + +@pytest.mark.parametrize('use_add_tile_args', [True, False]) +def testFrameValuesAddTile(use_add_tile_args, tmp_path): + output_file = tmp_path / 'test.db' + sink = large_image_source_zarr.new() + + frame_shape = (300, 400, 3) + expected = dict( + z=dict(values=[2, 4, 6, 8], uniform=True, units='m', stride=9, dtype='int64'), + t=dict(values=[10.0, 20.0, 30.0], uniform=False, units='ms', stride=3, dtype='float64'), + c=dict(values=['r', 'g', 'b'], uniform=True, units='channel', stride=1, dtype='str32'), + ) + expected_metadata = dict( + levels=1, + sizeY=frame_shape[0], + sizeX=frame_shape[1], + bandCount=frame_shape[2], + frames=[], + tileWidth=512, + tileHeight=512, + magnification=None, + mm_x=0, + mm_y=0, + dtype='float64', + channels=[f'Band {c + 1}' for c in range(len(expected['c']['values']))], + channelmap={f'Band {c + 1}': c for c in range(len(expected['c']['values']))}, + IndexRange={ + f'Index{k.upper()}': len(v['values']) for k, v in expected.items() + }, + IndexStride={ + f'Index{k.upper()}': v['stride'] for k, v in expected.items() + }, + **{ + f'Value{k.upper()}': dict( + values=v['values'], + units=v['units'], + uniform=v['uniform'], + min=min(v['values']), + max=max(v['values']), + datatype=v['dtype'], + ) for k, v in expected.items() + } + ) + + sink.frameAxes = list(expected.keys()) + sink.frameUnits = { + k: v['units'] for k, v in expected.items() + } + frame_values_shape = [ + *[len(v['values']) for v in expected.values()], + len(expected) + ] + frame_values = np.empty(frame_values_shape, dtype=object) + + frame = 0 + index = 0 + for z, z_value in enumerate(expected['z']['values']): + for t, t_value in enumerate(expected['t']['values']): + if not expected['t']['uniform']: + t_value += 0.01 * z + for c, c_value in enumerate(expected['c']['values']): + add_tile_args = dict(z=z, t=t, c=c, axes=['z', 't', 'c', 'y', 'x', 's']) + if use_add_tile_args: + add_tile_args.update(z_value=z_value, t_value=t_value, c_value=c_value) + else: + frame_values[z, t, c] = [z_value, t_value, c_value] + random_tile = np.random.random(frame_shape) + sink.addTile(random_tile, 0, 0, **add_tile_args) + expected_metadata['frames'].append( + dict( + Frame=frame, + Index=index, + IndexZ=z, + ValueZ=z_value, + IndexT=t, + ValueT=t_value, + IndexC=c, + ValueC=c_value, + Channel=f'Band {c + 1}', + ) + ) + frame += 1 + index += 1 + + if not use_add_tile_args: + sink.frameValues = frame_values + + compare_metadata(dict(sink.getMetadata()), expected_metadata) + + # sink.write(output_file) + # written = large_image_source_zarr.open(output_file) + # compare_metadata(dict(written.getMetadata()), expected_metadata) From 68666f809ed264a70a14fef2eaef7af1f7fe55ff Mon Sep 17 00:00:00 2001 From: Anne Haley Date: Wed, 4 Sep 2024 18:47:44 +0000 Subject: [PATCH 002/103] write frame values to internal metadata and reconstruct on read --- .../zarr/large_image_source_zarr/__init__.py | 69 +++++++++++++++++-- test/test_sink.py | 11 ++- 2 files changed, 68 insertions(+), 12 deletions(-) diff --git a/sources/zarr/large_image_source_zarr/__init__.py b/sources/zarr/large_image_source_zarr/__init__.py index 54dd569bf..b717a0524 100644 --- a/sources/zarr/large_image_source_zarr/__init__.py +++ b/sources/zarr/large_image_source_zarr/__init__.py @@ -224,7 +224,7 @@ def _scanZarrArray(self, group, arr, results): tuple that is used to find the maximum size array, preferring ome arrays, then total pixels, then channels. 'is_ome' is a boolean. 'series' is a list of the found groups and arrays that match the - best criteria. 'axes' and 'channels' are from the best array. + best criteria. 'axes', 'axes_values', 'axes_units', and 'channels' are from the best array. 'associated' is a list of all groups and arrays that might be associated images. These have to be culled for the actual groups used in the series. @@ -239,9 +239,13 @@ def _scanZarrArray(self, group, arr, results): all(packaging.version.Version(m['version']) >= min_version for m in attrs['multiscales'] if 'version' in m)) channels = None + axes_values = None + axes_units = None if is_ome: axes = {axis['name']: idx for idx, axis in enumerate( attrs['multiscales'][0]['axes'])} + axes_values = {axis['name']: axis.get('values') for axis in attrs['multiscales'][0]['axes']} + axes_units = {axis['name']: axis.get('unit') for axis in attrs['multiscales'][0]['axes']} if isinstance(attrs['omero'].get('channels'), list): channels = [channel['label'] for channel in attrs['omero']['channels']] if all(channel.startswith('Channel ') for channel in channels): @@ -260,6 +264,8 @@ def _scanZarrArray(self, group, arr, results): results['series'] = [(group, arr)] results['is_ome'] = is_ome results['axes'] = axes + results['axes_values'] = axes_values + results['axes_units'] = axes_units results['channels'] = channels elif check == results['best']: results['series'].append((group, arr)) @@ -404,6 +410,40 @@ def _validateZarr(self): self._axisCounts['xy'] = len(self._series) stride *= len(self._series) self._framecount = stride + axes_values = found.get('axes_values') + axes_units = found.get('axes_units') + if isinstance(axes_values, dict): + self._frameAxes = [ + a for a, i in sorted(self._axes.items(), key=lambda x: x[1]) + if axes_values.get(a) is not None + ] + self._frameUnits = {k: axes_units.get(k) for k in self.frameAxes if k in axes_units} + frame_values_shape = [baseArray.shape[self._axes[a]] for a in self.frameAxes] + frame_values_shape.append(len(frame_values_shape)) + frame_values = np.empty(frame_values_shape, dtype=object) + for axis, values in axes_values.items(): + if axis in self.frameAxes: + slicing = [slice(None) for i in range(len(frame_values_shape))] + axis_index = self.frameAxes.index(axis) + slicing[-1] = axis_index + if isinstance(values, list): + # uniform values are written as lists + for i, value in enumerate(values): + slicing[axis_index] = i + frame_values[tuple(slicing)] = value + elif isinstance(values, dict): + # non-uniform values are written as dicts mapping values to index permutations + for value, frame_specs in values.items(): + if isinstance(value, str): + try: + value = float(value) if '.' in value else int(value) + except: + pass + for frame_spec in frame_specs: + for a, i in frame_spec.items(): + slicing[self.frameAxes.index(a)] = i + frame_values[tuple(slicing)] = value + self._frameValues = frame_values def _nonemptyLevelsList(self, frame=0): """ @@ -773,9 +813,26 @@ def _writeInternalMetadata(self): rdefs['defaultT'] = 0 elif a == 'z': rdefs['defaultZ'] = 0 - unit = self.frameUnits.get(a) if self.frameUnits is not None else None - if unit is not None: - axis_metadata['unit'] = unit + if self.frameAxes is not None: + frame_axis_index = self.frameAxes.index(a) if a in self.frameAxes else None + if frame_axis_index is not None and self.frameValues is not None: + all_frame_values = self.frameValues[..., frame_axis_index] + split = np.split(all_frame_values, all_frame_values.shape[frame_axis_index], axis=frame_axis_index) + uniform = all(len(np.unique(a)) == 1 for a in split) + if uniform: + values = [a.flat[0] for a in split] + else: + values = {} + for index, value in np.ndenumerate(all_frame_values): + if value not in values: + values[value] = [] + values[value].append({ + axis: index[i] for i, axis in enumerate(self.frameAxes) + }) + axis_metadata['values'] = values + unit = self.frameUnits.get(a) if self.frameUnits is not None else None + if unit is not None: + axis_metadata['unit'] = unit axes.append(axis_metadata) if channel_axis is not None and len(arrays) > 0: base_array = list(arrays.values())[0] @@ -919,7 +976,7 @@ def frameAxes(self): def frameAxes(self, axes): self._checkEditable() self._frameAxes = axes - + @property def frameUnits(self): return self._frameUnits @@ -950,7 +1007,7 @@ def frameValues(self, a): err = f'frameValues must have {len(self.frameAxes) + 1} dimensions.' raise ValueError(err) self._frameValues = a - + def _generateDownsampledLevels(self, resample_method): self._checkEditable() current_arrays = dict(self._zarr.arrays()) diff --git a/test/test_sink.py b/test/test_sink.py index 3bd5dd74b..28082891a 100644 --- a/test/test_sink.py +++ b/test/test_sink.py @@ -557,8 +557,8 @@ def testFrameValuesAddTile(use_add_tile_args, tmp_path): frame_shape = (300, 400, 3) expected = dict( - z=dict(values=[2, 4, 6, 8], uniform=True, units='m', stride=9, dtype='int64'), - t=dict(values=[10.0, 20.0, 30.0], uniform=False, units='ms', stride=3, dtype='float64'), + z=dict(values=[2, 4, 6, 8], uniform=True, units='meter', stride=9, dtype='int64'), + t=dict(values=[10.0, 20.0, 30.0], uniform=False, units='millisecond', stride=3, dtype='float64'), c=dict(values=['r', 'g', 'b'], uniform=True, units='channel', stride=1, dtype='str32'), ) expected_metadata = dict( @@ -635,9 +635,8 @@ def testFrameValuesAddTile(use_add_tile_args, tmp_path): if not use_add_tile_args: sink.frameValues = frame_values - compare_metadata(dict(sink.getMetadata()), expected_metadata) - # sink.write(output_file) - # written = large_image_source_zarr.open(output_file) - # compare_metadata(dict(written.getMetadata()), expected_metadata) + sink.write(output_file) + written = large_image_source_zarr.open(output_file) + compare_metadata(dict(written.getMetadata()), expected_metadata) From d02dd55b50748137830e32247cae660bb1e35c7d Mon Sep 17 00:00:00 2001 From: Anne Haley Date: Wed, 4 Sep 2024 18:57:33 +0000 Subject: [PATCH 003/103] refactor test and add small test --- test/test_sink.py | 131 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 93 insertions(+), 38 deletions(-) diff --git a/test/test_sink.py b/test/test_sink.py index 28082891a..5434a8542 100644 --- a/test/test_sink.py +++ b/test/test_sink.py @@ -537,31 +537,8 @@ def testConcurrency(tmp_path): assert np.allclose(data, seed_data) -def compare_metadata(actual, expected): - assert type(actual) is type(expected) - if isinstance(actual, list): - for i, v in enumerate(actual): - compare_metadata(v, expected[i]) - elif isinstance(actual, dict): - assert len(actual.keys()) == len(expected.keys()) - for k, v in actual.items(): - compare_metadata(v, expected[k]) - else: - assert actual == expected - - -@pytest.mark.parametrize('use_add_tile_args', [True, False]) -def testFrameValuesAddTile(use_add_tile_args, tmp_path): - output_file = tmp_path / 'test.db' - sink = large_image_source_zarr.new() - - frame_shape = (300, 400, 3) - expected = dict( - z=dict(values=[2, 4, 6, 8], uniform=True, units='meter', stride=9, dtype='int64'), - t=dict(values=[10.0, 20.0, 30.0], uniform=False, units='millisecond', stride=3, dtype='float64'), - c=dict(values=['r', 'g', 'b'], uniform=True, units='channel', stride=1, dtype='str32'), - ) - expected_metadata = dict( +def get_expected_metadata(axis_spec, frame_shape): + return dict( levels=1, sizeY=frame_shape[0], sizeX=frame_shape[1], @@ -573,13 +550,13 @@ def testFrameValuesAddTile(use_add_tile_args, tmp_path): mm_x=0, mm_y=0, dtype='float64', - channels=[f'Band {c + 1}' for c in range(len(expected['c']['values']))], - channelmap={f'Band {c + 1}': c for c in range(len(expected['c']['values']))}, + channels=[f'Band {c + 1}' for c in range(len(axis_spec['c']['values']))], + channelmap={f'Band {c + 1}': c for c in range(len(axis_spec['c']['values']))}, IndexRange={ - f'Index{k.upper()}': len(v['values']) for k, v in expected.items() + f'Index{k.upper()}': len(v['values']) for k, v in axis_spec.items() }, IndexStride={ - f'Index{k.upper()}': v['stride'] for k, v in expected.items() + f'Index{k.upper()}': v['stride'] for k, v in axis_spec.items() }, **{ f'Value{k.upper()}': dict( @@ -589,27 +566,105 @@ def testFrameValuesAddTile(use_add_tile_args, tmp_path): min=min(v['values']), max=max(v['values']), datatype=v['dtype'], - ) for k, v in expected.items() + ) for k, v in axis_spec.items() } ) - sink.frameAxes = list(expected.keys()) +def compare_metadata(actual, expected): + assert type(actual) is type(expected) + if isinstance(actual, list): + for i, v in enumerate(actual): + compare_metadata(v, expected[i]) + elif isinstance(actual, dict): + assert len(actual.keys()) == len(expected.keys()) + for k, v in actual.items(): + compare_metadata(v, expected[k]) + else: + assert actual == expected + + +@pytest.mark.parametrize('use_add_tile_args', [True, False]) +def testFrameValuesSmall(use_add_tile_args, tmp_path): + output_file = tmp_path / 'test.db' + sink = large_image_source_zarr.new() + + frame_shape = (300, 400, 3) + axis_spec = dict( + c=dict(values=['r', 'g', 'b'], uniform=True, units='channel', stride=1, dtype='str32'), + ) + expected_metadata = get_expected_metadata(axis_spec, frame_shape) + + sink.frameAxes = list(axis_spec.keys()) + sink.frameUnits = { + k: v['units'] for k, v in axis_spec.items() + } + frame_values_shape = [ + *[len(v['values']) for v in axis_spec.values()], + len(axis_spec) + ] + frame_values = np.empty(frame_values_shape, dtype=object) + + frame = 0 + index = 0 + for c, c_value in enumerate(axis_spec['c']['values']): + add_tile_args = dict(c=c, axes=['c', 'y', 'x', 's']) + if use_add_tile_args: + add_tile_args.update(c_value=c_value) + else: + frame_values[c] = [c_value] + random_tile = np.random.random(frame_shape) + sink.addTile(random_tile, 0, 0, **add_tile_args) + expected_metadata['frames'].append( + dict( + Frame=frame, + Index=index, + IndexC=c, + ValueC=c_value, + Channel=f'Band {c + 1}', + ) + ) + frame += 1 + index += 1 + + if not use_add_tile_args: + sink.frameValues = frame_values + compare_metadata(dict(sink.getMetadata()), expected_metadata) + + sink.write(output_file) + written = large_image_source_zarr.open(output_file) + compare_metadata(dict(written.getMetadata()), expected_metadata) + + +@pytest.mark.parametrize('use_add_tile_args', [True, False]) +def testFrameValues(use_add_tile_args, tmp_path): + output_file = tmp_path / 'test.db' + sink = large_image_source_zarr.new() + + frame_shape = (300, 400, 3) + axis_spec = dict( + z=dict(values=[2, 4, 6, 8], uniform=True, units='meter', stride=9, dtype='int64'), + t=dict(values=[10.0, 20.0, 30.0], uniform=False, units='millisecond', stride=3, dtype='float64'), + c=dict(values=['r', 'g', 'b'], uniform=True, units='channel', stride=1, dtype='str32'), + ) + expected_metadata = get_expected_metadata(axis_spec, frame_shape) + + sink.frameAxes = list(axis_spec.keys()) sink.frameUnits = { - k: v['units'] for k, v in expected.items() + k: v['units'] for k, v in axis_spec.items() } frame_values_shape = [ - *[len(v['values']) for v in expected.values()], - len(expected) + *[len(v['values']) for v in axis_spec.values()], + len(axis_spec) ] frame_values = np.empty(frame_values_shape, dtype=object) frame = 0 index = 0 - for z, z_value in enumerate(expected['z']['values']): - for t, t_value in enumerate(expected['t']['values']): - if not expected['t']['uniform']: + for z, z_value in enumerate(axis_spec['z']['values']): + for t, t_value in enumerate(axis_spec['t']['values']): + if not axis_spec['t']['uniform']: t_value += 0.01 * z - for c, c_value in enumerate(expected['c']['values']): + for c, c_value in enumerate(axis_spec['c']['values']): add_tile_args = dict(z=z, t=t, c=c, axes=['z', 't', 'c', 'y', 'x', 's']) if use_add_tile_args: add_tile_args.update(z_value=z_value, t_value=t_value, c_value=c_value) From 0aa76b40173aba39183dae63c240965f54de95ed Mon Sep 17 00:00:00 2001 From: Anne Haley Date: Wed, 4 Sep 2024 19:16:31 +0000 Subject: [PATCH 004/103] Reformat, break up complex functions --- .../zarr/large_image_source_zarr/__init__.py | 162 ++++++++++-------- test/test_sink.py | 40 ++++- 2 files changed, 121 insertions(+), 81 deletions(-) diff --git a/sources/zarr/large_image_source_zarr/__init__.py b/sources/zarr/large_image_source_zarr/__init__.py index b717a0524..e0c178ec9 100644 --- a/sources/zarr/large_image_source_zarr/__init__.py +++ b/sources/zarr/large_image_source_zarr/__init__.py @@ -224,10 +224,10 @@ def _scanZarrArray(self, group, arr, results): tuple that is used to find the maximum size array, preferring ome arrays, then total pixels, then channels. 'is_ome' is a boolean. 'series' is a list of the found groups and arrays that match the - best criteria. 'axes', 'axes_values', 'axes_units', and 'channels' are from the best array. - 'associated' is a list of all groups and arrays that might be - associated images. These have to be culled for the actual groups - used in the series. + best criteria. 'axes', 'axes_values', 'axes_units', and 'channels' + are from the best array. 'associated' is a list of all groups and + arrays that might be associated images. These have to be culled + for the actual groups used in the series. """ attrs = group.attrs.asdict() if group is not None else {} min_version = packaging.version.Version('0.4') @@ -244,8 +244,14 @@ def _scanZarrArray(self, group, arr, results): if is_ome: axes = {axis['name']: idx for idx, axis in enumerate( attrs['multiscales'][0]['axes'])} - axes_values = {axis['name']: axis.get('values') for axis in attrs['multiscales'][0]['axes']} - axes_units = {axis['name']: axis.get('unit') for axis in attrs['multiscales'][0]['axes']} + axes_values = { + axis['name']: axis.get('values') + for axis in attrs['multiscales'][0]['axes'] + } + axes_units = { + axis['name']: axis.get('unit') + for axis in attrs['multiscales'][0]['axes'] + } if isinstance(attrs['omero'].get('channels'), list): channels = [channel['label'] for channel in attrs['omero']['channels']] if all(channel.startswith('Channel ') for channel in channels): @@ -345,6 +351,43 @@ def _getScale(self): except Exception: pass + def _readFrameValues(self, found, baseArray): + axes_values = found.get('axes_values') + axes_units = found.get('axes_units') + if isinstance(axes_values, dict): + self._frameAxes = [ + a for a, i in sorted(self._axes.items(), key=lambda x: x[1]) + if axes_values.get(a) is not None + ] + self._frameUnits = {k: axes_units.get(k) for k in self.frameAxes if k in axes_units} + frame_values_shape = [baseArray.shape[self._axes[a]] for a in self.frameAxes] + frame_values_shape.append(len(frame_values_shape)) + frame_values = np.empty(frame_values_shape, dtype=object) + for axis, values in axes_values.items(): + if axis in self.frameAxes: + slicing = [slice(None) for i in range(len(frame_values_shape))] + axis_index = self.frameAxes.index(axis) + slicing[-1] = axis_index + if isinstance(values, list): + # uniform values are written as lists + for i, value in enumerate(values): + slicing[axis_index] = i + frame_values[tuple(slicing)] = value + elif isinstance(values, dict): + # non-uniform values are written as dicts + # mapping values to index permutations + for value, frame_specs in values.items(): + if isinstance(value, str): + try: + value = float(value) if '.' in value else int(value) + except Exception: + pass + for frame_spec in frame_specs: + for a, i in frame_spec.items(): + slicing[self.frameAxes.index(a)] = i + frame_values[tuple(slicing)] = value + self._frameValues = frame_values + def _validateZarr(self): """ Validate that we can read tiles from the zarr parent group in @@ -410,40 +453,7 @@ def _validateZarr(self): self._axisCounts['xy'] = len(self._series) stride *= len(self._series) self._framecount = stride - axes_values = found.get('axes_values') - axes_units = found.get('axes_units') - if isinstance(axes_values, dict): - self._frameAxes = [ - a for a, i in sorted(self._axes.items(), key=lambda x: x[1]) - if axes_values.get(a) is not None - ] - self._frameUnits = {k: axes_units.get(k) for k in self.frameAxes if k in axes_units} - frame_values_shape = [baseArray.shape[self._axes[a]] for a in self.frameAxes] - frame_values_shape.append(len(frame_values_shape)) - frame_values = np.empty(frame_values_shape, dtype=object) - for axis, values in axes_values.items(): - if axis in self.frameAxes: - slicing = [slice(None) for i in range(len(frame_values_shape))] - axis_index = self.frameAxes.index(axis) - slicing[-1] = axis_index - if isinstance(values, list): - # uniform values are written as lists - for i, value in enumerate(values): - slicing[axis_index] = i - frame_values[tuple(slicing)] = value - elif isinstance(values, dict): - # non-uniform values are written as dicts mapping values to index permutations - for value, frame_specs in values.items(): - if isinstance(value, str): - try: - value = float(value) if '.' in value else int(value) - except: - pass - for frame_spec in frame_specs: - for a, i in frame_spec.items(): - slicing[self.frameAxes.index(a)] = i - frame_values[tuple(slicing)] = value - self._frameValues = frame_values + self._readFrameValues(found, baseArray) def _nonemptyLevelsList(self, frame=0): """ @@ -502,7 +512,7 @@ def getMetadata(self): units=self.frameUnits.get(axis) if self.frameUnits is not None else None, min=min(values), max=max(values), - datatype=np.array(values).dtype.name + datatype=np.array(values).dtype.name, ) for idx in range(self._framecount): frame = {'Frame': idx} @@ -697,7 +707,7 @@ def addTile(self, tile, x=0, y=0, mask=None, axes=None, **kwargs): elif self.frameValues.shape != frames_shape: self.frameValues = np.pad( self.frameValues, - [(0, s - self.frameValues.shape[i]) for i, s in enumerate(frames_shape)] + [(0, s - self.frameValues.shape[i]) for i, s in enumerate(frames_shape)], ) current_frame_slice = tuple(placement.get(a) for a in self.frameAxes) for i, k in enumerate(self.frameAxes): @@ -778,6 +788,39 @@ def addAssociatedImage(self, image, imageKey=None): ) self._associatedImages[imageKey] = (group, arr) + def _getAxisInternalMetadata(self, axis_name): + axis_metadata = {'name': axis_name} + if axis_name in ['x', 'y']: + axis_metadata['type'] = 'space' + axis_metadata['unit'] = 'millimeter' + elif axis_name in ['s', 'c']: + axis_metadata['type'] = 'channel' + if self.frameAxes is not None: + frame_axis_index = self.frameAxes.index(axis_name) if axis_name in self.frameAxes else None + if frame_axis_index is not None and self.frameValues is not None: + all_frame_values = self.frameValues[..., frame_axis_index] + split = np.split( + all_frame_values, + all_frame_values.shape[frame_axis_index], + axis=frame_axis_index, + ) + uniform = all(len(np.unique(a)) == 1 for a in split) + if uniform: + values = [a.flat[0] for a in split] + else: + values = {} + for index, value in np.ndenumerate(all_frame_values): + if value not in values: + values[value] = [] + values[value].append({ + axis: index[i] for i, axis in enumerate(self.frameAxes) + }) + axis_metadata['values'] = values + unit = self.frameUnits.get(axis_name) if self.frameUnits is not None else None + if unit is not None: + axis_metadata['unit'] = unit + return axis_metadata + def _writeInternalMetadata(self): self._checkEditable() with self._threadLock and self._processLock: @@ -803,37 +846,11 @@ def _writeInternalMetadata(self): } datasets.append(dataset_metadata) for a in sorted_axes: - axis_metadata = {'name': a} - if a in ['x', 'y']: - axis_metadata['type'] = 'space' - axis_metadata['unit'] = 'millimeter' - elif a in ['s', 'c']: - axis_metadata['type'] = 'channel' - elif a == 't': + if a == 't': rdefs['defaultT'] = 0 elif a == 'z': rdefs['defaultZ'] = 0 - if self.frameAxes is not None: - frame_axis_index = self.frameAxes.index(a) if a in self.frameAxes else None - if frame_axis_index is not None and self.frameValues is not None: - all_frame_values = self.frameValues[..., frame_axis_index] - split = np.split(all_frame_values, all_frame_values.shape[frame_axis_index], axis=frame_axis_index) - uniform = all(len(np.unique(a)) == 1 for a in split) - if uniform: - values = [a.flat[0] for a in split] - else: - values = {} - for index, value in np.ndenumerate(all_frame_values): - if value not in values: - values[value] = [] - values[value].append({ - axis: index[i] for i, axis in enumerate(self.frameAxes) - }) - axis_metadata['values'] = values - unit = self.frameUnits.get(a) if self.frameUnits is not None else None - if unit is not None: - axis_metadata['unit'] = unit - axes.append(axis_metadata) + axes.append(self._getAxisInternalMetadata(a)) if channel_axis is not None and len(arrays) > 0: base_array = list(arrays.values())[0] base_shape = base_array.shape @@ -986,11 +1003,12 @@ def frameUnits(self, units): self._checkEditable() if self.frameAxes is None: err = 'frameAxes must be set first with a list of frame axis names.' - raise ValueError + raise ValueError(err) if not isinstance(units, dict) or not all( k in self.frameAxes for k in units.keys() ): err = 'frameUnits must be a dictionary with keys that exist in frameAxes.' + raise ValueError(err) self._frameUnits = units @property @@ -1002,7 +1020,7 @@ def frameValues(self, a): self._checkEditable() if self.frameAxes is None: err = 'frameAxes must be set first with a list of frame axis names.' - raise ValueError + raise ValueError(err) if len(a.shape) != len(self.frameAxes) + 1: err = f'frameValues must have {len(self.frameAxes) + 1} dimensions.' raise ValueError(err) diff --git a/test/test_sink.py b/test/test_sink.py index 5434a8542..e28b4b929 100644 --- a/test/test_sink.py +++ b/test/test_sink.py @@ -567,7 +567,7 @@ def get_expected_metadata(axis_spec, frame_shape): max=max(v['values']), datatype=v['dtype'], ) for k, v in axis_spec.items() - } + }, ) def compare_metadata(actual, expected): @@ -589,9 +589,13 @@ def testFrameValuesSmall(use_add_tile_args, tmp_path): sink = large_image_source_zarr.new() frame_shape = (300, 400, 3) - axis_spec = dict( - c=dict(values=['r', 'g', 'b'], uniform=True, units='channel', stride=1, dtype='str32'), - ) + axis_spec = dict(c=dict( + values=['r', 'g', 'b'], + uniform=True, + units='channel', + stride=1, + dtype='str32', + )) expected_metadata = get_expected_metadata(axis_spec, frame_shape) sink.frameAxes = list(axis_spec.keys()) @@ -600,7 +604,7 @@ def testFrameValuesSmall(use_add_tile_args, tmp_path): } frame_values_shape = [ *[len(v['values']) for v in axis_spec.values()], - len(axis_spec) + len(axis_spec), ] frame_values = np.empty(frame_values_shape, dtype=object) @@ -621,7 +625,7 @@ def testFrameValuesSmall(use_add_tile_args, tmp_path): IndexC=c, ValueC=c_value, Channel=f'Band {c + 1}', - ) + ), ) frame += 1 index += 1 @@ -642,9 +646,27 @@ def testFrameValues(use_add_tile_args, tmp_path): frame_shape = (300, 400, 3) axis_spec = dict( - z=dict(values=[2, 4, 6, 8], uniform=True, units='meter', stride=9, dtype='int64'), - t=dict(values=[10.0, 20.0, 30.0], uniform=False, units='millisecond', stride=3, dtype='float64'), - c=dict(values=['r', 'g', 'b'], uniform=True, units='channel', stride=1, dtype='str32'), + z=dict( + values=[2, 4, 6, 8], + uniform=True, + units='meter', + stride=9, + dtype='int64', + ), + t=dict( + values=[10.0, 20.0, 30.0], + uniform=False, + units='millisecond', + stride=3, + dtype='float64', + ), + c=dict( + values=['r', 'g', 'b'], + uniform=True, + units='channel', + stride=1, + dtype='str32', + ), ) expected_metadata = get_expected_metadata(axis_spec, frame_shape) From 6c00eed5af2d7312bbca6638aa36c99b49dcb6fb Mon Sep 17 00:00:00 2001 From: Anne Haley Date: Wed, 4 Sep 2024 19:24:15 +0000 Subject: [PATCH 005/103] Fix styling --- .../zarr/large_image_source_zarr/__init__.py | 26 +++--- test/test_sink.py | 79 ++++++++++--------- 2 files changed, 53 insertions(+), 52 deletions(-) diff --git a/sources/zarr/large_image_source_zarr/__init__.py b/sources/zarr/large_image_source_zarr/__init__.py index e0c178ec9..621f1db9a 100644 --- a/sources/zarr/large_image_source_zarr/__init__.py +++ b/sources/zarr/large_image_source_zarr/__init__.py @@ -225,8 +225,8 @@ def _scanZarrArray(self, group, arr, results): arrays, then total pixels, then channels. 'is_ome' is a boolean. 'series' is a list of the found groups and arrays that match the best criteria. 'axes', 'axes_values', 'axes_units', and 'channels' - are from the best array. 'associated' is a list of all groups and - arrays that might be associated images. These have to be culled + are from the best array. 'associated' is a list of all groups and + arrays that might be associated images. These have to be culled for the actual groups used in the series. """ attrs = group.attrs.asdict() if group is not None else {} @@ -245,11 +245,11 @@ def _scanZarrArray(self, group, arr, results): axes = {axis['name']: idx for idx, axis in enumerate( attrs['multiscales'][0]['axes'])} axes_values = { - axis['name']: axis.get('values') + axis['name']: axis.get('values') for axis in attrs['multiscales'][0]['axes'] } axes_units = { - axis['name']: axis.get('unit') + axis['name']: axis.get('unit') for axis in attrs['multiscales'][0]['axes'] } if isinstance(attrs['omero'].get('channels'), list): @@ -374,7 +374,7 @@ def _readFrameValues(self, found, baseArray): slicing[axis_index] = i frame_values[tuple(slicing)] = value elif isinstance(values, dict): - # non-uniform values are written as dicts + # non-uniform values are written as dicts # mapping values to index permutations for value, frame_specs in values.items(): if isinstance(value, str): @@ -387,7 +387,7 @@ def _readFrameValues(self, found, baseArray): slicing[self.frameAxes.index(a)] = i frame_values[tuple(slicing)] = value self._frameValues = frame_values - + def _validateZarr(self): """ Validate that we can read tiles from the zarr parent group in @@ -796,13 +796,13 @@ def _getAxisInternalMetadata(self, axis_name): elif axis_name in ['s', 'c']: axis_metadata['type'] = 'channel' if self.frameAxes is not None: - frame_axis_index = self.frameAxes.index(axis_name) if axis_name in self.frameAxes else None - if frame_axis_index is not None and self.frameValues is not None: - all_frame_values = self.frameValues[..., frame_axis_index] + axis_index = self.frameAxes.index(axis_name) if axis_name in self.frameAxes else None + if axis_index is not None and self.frameValues is not None: + all_frame_values = self.frameValues[..., axis_index] split = np.split( all_frame_values, - all_frame_values.shape[frame_axis_index], - axis=frame_axis_index, + all_frame_values.shape[axis_index], + axis=axis_index, ) uniform = all(len(np.unique(a)) == 1 for a in split) if uniform: @@ -819,8 +819,8 @@ def _getAxisInternalMetadata(self, axis_name): unit = self.frameUnits.get(axis_name) if self.frameUnits is not None else None if unit is not None: axis_metadata['unit'] = unit - return axis_metadata - + return axis_metadata + def _writeInternalMetadata(self): self._checkEditable() with self._threadLock and self._processLock: diff --git a/test/test_sink.py b/test/test_sink.py index e28b4b929..e2634d1dd 100644 --- a/test/test_sink.py +++ b/test/test_sink.py @@ -570,6 +570,7 @@ def get_expected_metadata(axis_spec, frame_shape): }, ) + def compare_metadata(actual, expected): assert type(actual) is type(expected) if isinstance(actual, list): @@ -611,23 +612,23 @@ def testFrameValuesSmall(use_add_tile_args, tmp_path): frame = 0 index = 0 for c, c_value in enumerate(axis_spec['c']['values']): - add_tile_args = dict(c=c, axes=['c', 'y', 'x', 's']) - if use_add_tile_args: - add_tile_args.update(c_value=c_value) - else: - frame_values[c] = [c_value] - random_tile = np.random.random(frame_shape) - sink.addTile(random_tile, 0, 0, **add_tile_args) - expected_metadata['frames'].append( - dict( - Frame=frame, - Index=index, - IndexC=c, - ValueC=c_value, - Channel=f'Band {c + 1}', - ), - ) - frame += 1 + add_tile_args = dict(c=c, axes=['c', 'y', 'x', 's']) + if use_add_tile_args: + add_tile_args.update(c_value=c_value) + else: + frame_values[c] = [c_value] + random_tile = np.random.random(frame_shape) + sink.addTile(random_tile, 0, 0, **add_tile_args) + expected_metadata['frames'].append( + dict( + Frame=frame, + Index=index, + IndexC=c, + ValueC=c_value, + Channel=f'Band {c + 1}', + ), + ) + frame += 1 index += 1 if not use_add_tile_args: @@ -676,7 +677,7 @@ def testFrameValues(use_add_tile_args, tmp_path): } frame_values_shape = [ *[len(v['values']) for v in axis_spec.values()], - len(axis_spec) + len(axis_spec), ] frame_values = np.empty(frame_values_shape, dtype=object) @@ -687,27 +688,27 @@ def testFrameValues(use_add_tile_args, tmp_path): if not axis_spec['t']['uniform']: t_value += 0.01 * z for c, c_value in enumerate(axis_spec['c']['values']): - add_tile_args = dict(z=z, t=t, c=c, axes=['z', 't', 'c', 'y', 'x', 's']) - if use_add_tile_args: - add_tile_args.update(z_value=z_value, t_value=t_value, c_value=c_value) - else: - frame_values[z, t, c] = [z_value, t_value, c_value] - random_tile = np.random.random(frame_shape) - sink.addTile(random_tile, 0, 0, **add_tile_args) - expected_metadata['frames'].append( - dict( - Frame=frame, - Index=index, - IndexZ=z, - ValueZ=z_value, - IndexT=t, - ValueT=t_value, - IndexC=c, - ValueC=c_value, - Channel=f'Band {c + 1}', - ) - ) - frame += 1 + add_tile_args = dict(z=z, t=t, c=c, axes=['z', 't', 'c', 'y', 'x', 's']) + if use_add_tile_args: + add_tile_args.update(z_value=z_value, t_value=t_value, c_value=c_value) + else: + frame_values[z, t, c] = [z_value, t_value, c_value] + random_tile = np.random.random(frame_shape) + sink.addTile(random_tile, 0, 0, **add_tile_args) + expected_metadata['frames'].append( + dict( + Frame=frame, + Index=index, + IndexZ=z, + ValueZ=z_value, + IndexT=t, + ValueT=t_value, + IndexC=c, + ValueC=c_value, + Channel=f'Band {c + 1}', + ), + ) + frame += 1 index += 1 if not use_add_tile_args: From f00d36d166fcc29eed924f4aad07cbba4aa08abb Mon Sep 17 00:00:00 2001 From: David Manthey Date: Thu, 26 Sep 2024 13:35:54 -0400 Subject: [PATCH 006/103] Remove no longer used code; adjust item list slightly. With the addition of #1649, we always use the large_image itemList rather than modifying the base girder item list. We no longer need the code that did that modification. Don't display column titles on an empty list. Make check boxes easier to click. Allow long strings to break more efficiently. --- CHANGELOG.md | 1 + .../web_client/templates/itemList.pug | 37 ++++---- .../web_client/views/configView.js | 16 ++++ .../web_client/views/itemList.js | 87 ++----------------- 4 files changed, 47 insertions(+), 94 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b09dd6197..eff20b494 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Better handle images without enough tile layers ([#1648](../../pull/1648)) - Add users option to config files; have a default config file ([#1649](../../pull/1649)) +- Remove no longer used code; adjust item list slightly ([#1651](../../pull/1651)) ### Bug Fixes diff --git a/girder/girder_large_image/web_client/templates/itemList.pug b/girder/girder_large_image/web_client/templates/itemList.pug index 7028911d6..66f4b38b1 100644 --- a/girder/girder_large_image/web_client/templates/itemList.pug +++ b/girder/girder_large_image/web_client/templates/itemList.pug @@ -1,23 +1,24 @@ ul.g-item-list.li-item-list(layout_mode=(itemList.layout || {}).mode || '') - var colNames = []; - li.li-item-list-header - if checkboxes - span.li-item-list-header - for column, colidx in itemList.columns - if column.type !== 'image' || hasAnyLargeImage - span.li-item-list-header( - class=((column.type === 'record' && column.value !== 'controls') || column.type === 'metadata' ? 'sortable' : '') + ' ' + (sort && sort[0].type === column.type && ('' + sort[0].value === '' + column.value) ? sort[0].dir : ''), - column_type=column.type, column_value=column.value) - if column.title !== undefined - - colNames[colidx] = column.title - else - - colNames[colidx] = `${column.value.substr(0, 1).toUpperCase()}${column.value.substr(1)}` - = colNames[colidx] + if items.length + li.li-item-list-header + if checkboxes + span.li-item-list-header + for column, colidx in itemList.columns + if column.type !== 'image' || hasAnyLargeImage + span.li-item-list-header( + class=((column.type === 'record' && column.value !== 'controls') || column.type === 'metadata' ? 'sortable' : '') + ' ' + (sort && sort[0].type === column.type && ('' + sort[0].value === '' + column.value) ? sort[0].dir : ''), + column_type=column.type, column_value=column.value) + if column.title !== undefined + - colNames[colidx] = column.title + else + - colNames[colidx] = `${column.value.substr(0, 1).toUpperCase()}${column.value.substr(1)}` + = colNames[colidx] each item in items li.g-item-list-entry(class=(highlightItem && item.id === selectedItemId ? 'g-selected' : ''), public=(isParentPublic ? 'true' : 'false'), style=(itemList.layout || {}).mode == 'grid' ? ('max-width: ' + parseInt((itemList.layout || {})['max-width'] || 250) + 'px') : '') if checkboxes - span.li-item-list-cell - input.g-list-checkbox(type="checkbox", g-item-cid=item.cid) + label.li-item-list-cell(for='g-item-cid-' + item.cid) + input.g-list-checkbox(type="checkbox", g-item-cid=item.cid, id='g-item-cid-' + item.cid) for column, colidx in itemList.columns if column.type !== 'image' || hasAnyLargeImage - @@ -86,7 +87,11 @@ ul.g-item-list.li-item-list(layout_mode=(itemList.layout || {}).mode || '') input.input-sm.form-control.g-widget-metadata-value-input.g-widget-metadata-column(placeholder=column.description || "Value", value=value, title=column.description) else span.large_image_metadata - = value + if column.format === 'text' && value + //- allow long strings to be hyphenated at periods and underscores + != String(value).replace(/&/g, '&').replace(//, '>').replace(/"/, '"').replace(/'/, ''').replace(/\./g, '.­').replace(/_/g, '_­') + else + = value if value span.li-item-list-cell-filter(title="Only show items that match this metadata value exactly", filter-value=value, column-value=column.value) i.icon-filter diff --git a/girder/girder_large_image/web_client/views/configView.js b/girder/girder_large_image/web_client/views/configView.js index 078315957..9ffffd0b7 100644 --- a/girder/girder_large_image/web_client/views/configView.js +++ b/girder/girder_large_image/web_client/views/configView.js @@ -274,6 +274,22 @@ var ConfigView = View.extend({ callback(ConfigView._liconfig); } return val; + }).fail(() => { + // fallback matching server values + const li = { + columns: [ + {type: 'record', value: 'name', title: 'Name'}, + {type: 'record', value: 'controls', title: 'Contols'}, + {type: 'record', value: 'size', title: 'Size'}] + }; + const val = {itemList: li, itemListDialog: li}; + ConfigView._lastliconfig = folderId; + ConfigView._liconfigSettingsRequest = null; + ConfigView._liconfig = val; + if (callback) { + callback(ConfigView._liconfig); + } + return val; }); return ConfigView._liconfigSettingsRequest; }, diff --git a/girder/girder_large_image/web_client/views/itemList.js b/girder/girder_large_image/web_client/views/itemList.js index b1f36788a..4f659dff3 100644 --- a/girder/girder_large_image/web_client/views/itemList.js +++ b/girder/girder_large_image/web_client/views/itemList.js @@ -4,7 +4,6 @@ import Backbone from 'backbone'; import {wrap} from '@girder/core/utilities/PluginUtils'; import {getApiRoot} from '@girder/core/rest'; -import {getCurrentUser} from '@girder/core/auth'; import {AccessType} from '@girder/core/constants'; import {formatSize, parseQueryString, splitRoute} from '@girder/core/misc'; import HierarchyWidget from '@girder/core/views/widgets/HierarchyWidget'; @@ -73,16 +72,19 @@ wrap(ItemListWidget, 'initialize', function (initialize, settings) { }; }); update = true; + } else if (this._confList() && this._confList().defaultSort && this._confList().defaultSort.length) { + this._lastSort = this._confList().defaultSort; } if (query.filter || this._recurse) { this._generalFilter = query.filter; - this._setFilter(); + this._setFilter(false); update = true; } if (update) { this._setSort(); + } else { + this.render(); } - this.render(); }); this.events['click .li-item-list-header.sortable'] = (evt) => sortColumn.call(this, evt); this.events['click .li-item-list-cell-filter'] = (evt) => itemListCellFilter.call(this, evt); @@ -129,55 +131,6 @@ wrap(ItemListWidget, 'render', function (render) { } } - function addLargeImageDetails(item, container, parent, extraInfo) { - var elem; - elem = $('
'); - elem.attr('g-item-cid', item.cid); - container.append(elem); - /* We store the desired src attribute in deferred-src until we actually - * load the image. */ - elem.append($('').attr( - 'deferred-src', getApiRoot() + '/item/' + - item.id + '/tiles/thumbnail?width=160&height=100')); - var access = item.getAccessLevel(); - var extra = extraInfo[access] || extraInfo[AccessType.READ] || {}; - if (!getCurrentUser()) { - extra = extraInfo.null || {}; - } - - /* Set the maximum number of columns we have so that we can let css - * perform alignment. */ - var numColumns = Math.max((extra.images || []).length + 1, parent.attr('large_image_columns') || 0); - parent.attr('large_image_columns', numColumns); - - _.each(extra.images || [], function (imageName) { - elem = $('
'); - container.append(elem); - elem.append($('').attr( - 'deferred-src', getApiRoot() + '/item/' + item.id + - '/tiles/images/' + imageName + '?width=160&height=100&_=' + item.get('updated') - )); - elem.attr('extra-image', imageName); - }); - - $('.large_image_thumbnail', container).each(function () { - var elem = $(this); - /* Handle images loading or failing. */ - $('img', elem).one('error', function () { - $('img', elem).addClass('failed-to-load'); - $('img', elem).removeClass('loading waiting'); - elem.addClass('failed-to-load'); - _loadMoreImages(parent); - }); - $('img', elem).one('load', function () { - $('img', elem).addClass('loaded'); - $('img', elem).removeClass('loading waiting'); - _loadMoreImages(parent); - }); - }); - _loadMoreImages(parent); - } - this._confList = () => { return this._liconfig ? (this.$el.closest('.modal-dialog').length ? this._liconfig.itemListDialog : this._liconfig.itemList) : undefined; }; @@ -234,7 +187,7 @@ wrap(ItemListWidget, 'render', function (render) { return val; }; - this._setFilter = () => { + this._setFilter = (update) => { const val = this._generalFilter; let filter; const usedPhrases = {}; @@ -340,7 +293,9 @@ wrap(ItemListWidget, 'render', function (render) { this._filter = filter; this.collection.params = this.collection.params || {}; this.collection.params.text = this._filter; - this._setSort(); + if (update !== false) { + this._setSort(); + } } }; @@ -431,7 +386,6 @@ wrap(ItemListWidget, 'render', function (render) { largeImageConfig.getSettings((settings) => { var items = this.collection.toArray(); - var parent = this.$el; this._hasAnyLargeImage = !!_.some(items, function (item) { return item.has('largeImage'); }); @@ -442,29 +396,6 @@ wrap(ItemListWidget, 'render', function (render) { if (this._recurse && !((this.collection || {}).params || {}).text) { this._setFilter(); this.render(); - return; - } - render.call(this); - if (settings['large_image.show_thumbnails'] === false || - this.$('.large_image_container').length > 0) { - return this; - } - if (this._hasAnyLargeImage) { - if (!this._confList()) { - _.each(items, (item) => { - var elem = $('
'); - if (item.get('largeImage')) { - item.getAccessLevel(() => { - if (!this._confList()) { - addLargeImageDetails(item, elem, parent, settings.extraInfo); - } - }); - } - var inner = $('').html($('a[g-item-cid="' + item.cid + '"]').html()); - $('a[g-item-cid="' + item.cid + '"]', parent).first().empty().append(elem, inner); - _loadMoreImages(parent); - }); - } } return this; }); From c2d4cd03f3287da112eae514b0849c0f1cf1863a Mon Sep 17 00:00:00 2001 From: Jeffrey Baumes Date: Thu, 26 Sep 2024 14:37:15 -0400 Subject: [PATCH 007/103] New UI for creating and editing views --- .../web_client/package.json | 1 + .../web_client/stylesheets/itemList.styl | 81 ------ .../web_client/templates/itemList.pug | 226 ++++++++------- .../web_client/views/TableConfigDialog.vue | 131 +++++++++ .../web_client/views/TableViewSelect.vue | 85 ++++++ .../web_client/views/configView.js | 32 +++ .../web_client/views/itemList.js | 259 ++++++++++++++++-- 7 files changed, 621 insertions(+), 194 deletions(-) create mode 100644 girder/girder_large_image/web_client/views/TableConfigDialog.vue create mode 100644 girder/girder_large_image/web_client/views/TableViewSelect.vue diff --git a/girder/girder_large_image/web_client/package.json b/girder/girder_large_image/web_client/package.json index d25bdb33e..b208d1ed7 100644 --- a/girder/girder_large_image/web_client/package.json +++ b/girder/girder_large_image/web_client/package.json @@ -27,6 +27,7 @@ "vue-color": "^2.8.1", "vue-loader": "~15.9.8", "vue-template-compiler": "~2.6.14", + "vuedraggable": "^2.24.3", "webpack": "^3", "yaml": "^2.1.1" }, diff --git a/girder/girder_large_image/web_client/stylesheets/itemList.styl b/girder/girder_large_image/web_client/stylesheets/itemList.styl index a6ecbf659..48ce5bfdf 100644 --- a/girder/girder_large_image/web_client/stylesheets/itemList.styl +++ b/girder/girder_large_image/web_client/stylesheets/itemList.styl @@ -24,75 +24,6 @@ div.large_image_container+span div.large_image_container min-width 480px -ul.g-item-list - &.li-item-list - display table - background #fff - - >li - display table-row - - &.li-item-list-header - padding-bottom 5px - - >span.li-item-list-header - display table-cell - font-weight bold - text-align center - border-bottom 1px solid #888 - padding 0 5px - - &.sortable - &:after - color black - content "\002b65" - padding-left 5px - - &.down:after - content "\002b63" - - &.up:after - content "\002b61" - - >.li-item-list-cell - display table-cell - padding 4px 3px 3px - vertical-align top - - &.li-column-record-size - div - float none - margin-left 0 - padding 0 10px - - &.li-column-record-controls - white-space nowrap - - >span.li-item-list-cell:first-child - padding-left 5px - - >span.li-item-list-cell:last-child - padding-right 5px - - >span.li-column-metadata - padding-left 5px - padding-right 5px - - div.large_image_thumbnail - display flex - align-items flex-end - justify-content center - width inherit - - span.g-item-list-label - font-weight bold - font-size 14px - color black - padding-right 5px - - span.large_image_metadata - display inline-block - &.li-item-list[layout_mode="grid"] display block @@ -107,9 +38,6 @@ ul.g-item-list &>li>.li-item-list-cell display inline-block -.li-item-list-filter - padding-left 12px - @media (min-width 768px) .modal-dialog.li-item-list-dialog width inherit @@ -119,14 +47,5 @@ ul.g-item-list width 70% max-width 1000px -.li-flatten-item-list - font-size 14px - display inline-block - margin-left 20px - - label - padding-left 5px - font-weight normal - .li-item-list-filter-clear cursor pointer diff --git a/girder/girder_large_image/web_client/templates/itemList.pug b/girder/girder_large_image/web_client/templates/itemList.pug index 7028911d6..e628ba73f 100644 --- a/girder/girder_large_image/web_client/templates/itemList.pug +++ b/girder/girder_large_image/web_client/templates/itemList.pug @@ -1,97 +1,133 @@ -ul.g-item-list.li-item-list(layout_mode=(itemList.layout || {}).mode || '') +
- var colNames = []; - li.li-item-list-header - if checkboxes - span.li-item-list-header - for column, colidx in itemList.columns - if column.type !== 'image' || hasAnyLargeImage - span.li-item-list-header( - class=((column.type === 'record' && column.value !== 'controls') || column.type === 'metadata' ? 'sortable' : '') + ' ' + (sort && sort[0].type === column.type && ('' + sort[0].value === '' + column.value) ? sort[0].dir : ''), - column_type=column.type, column_value=column.value) - if column.title !== undefined - - colNames[colidx] = column.title - else - - colNames[colidx] = `${column.value.substr(0, 1).toUpperCase()}${column.value.substr(1)}` - = colNames[colidx] - each item in items - li.g-item-list-entry(class=(highlightItem && item.id === selectedItemId ? 'g-selected' : ''), public=(isParentPublic ? 'true' : 'false'), style=(itemList.layout || {}).mode == 'grid' ? ('max-width: ' + parseInt((itemList.layout || {})['max-width'] || 250) + 'px') : '') - if checkboxes - span.li-item-list-cell - input.g-list-checkbox(type="checkbox", g-item-cid=item.cid) - for column, colidx in itemList.columns - if column.type !== 'image' || hasAnyLargeImage - - - var divtype = column.type !== 'record' || column.value !== 'controls' ? 'a' : 'span'; - var classes = divtype == 'a' ? ['g-item-list-link']: []; - if (('' + column.type + column.value).match(/^[a-zA-Z][a-zA-Z0-9-_]*$/)) classes.push(`li-column-${column.type}-${column.value}`); - if (('' + column.type).match(/^[a-zA-Z][a-zA-Z0-9-_]*$/)) classes.push(`li-column-${column.type}`); - var skip = false; - (column.only || []).forEach((only) => { - if (!(only || {}).match) { - return; - } - var onlyval = (only.type === 'record' && only.value === 'name') ? item.name() : (only.type === 'record' && only.value === 'description') ? item.get(only.value) : ''; - if (only.type === 'metadata') { - onlyval = item.get('meta') || {}; - only.value.split('.').forEach((part) => { - onlyval = (onlyval || {})[part]; - }) - } - if (onlyval.match(new RegExp(only.match, 'i')) === null) { - skip = true; - } - }); - #{divtype}.li-item-list-cell(class=classes.join(' '), g-item-cid=item.cid, href=`#item/${item.id}`, title=colNames[colidx]) - if !skip && column.label - span.g-item-list-label - = column.label - if skip - //- ignore - else if column.type === 'record' - if column.value === 'name' - span.g-item-list-link - i.icon-doc-text-inv - = item.name() - else if column.value === 'controls' - if downloadLinks - a(title="Download item", href=item.downloadUrl()) - i.icon-download - if viewLinks - a.g-view-inline(title="View in browser", target="_blank", rel="noopener noreferrer", - href=item.downloadUrl({contentDisposition: 'inline'})) - i.icon-eye - else if column.value === 'size' - .g-item-size= formatSize(item.get('size')) - else if column.value === 'description' - = item.get(column.value) - else if column.type === 'image' && item.get('largeImage') - .large_image_thumbnail(extra-image=column.value !== 'thumbnail' ? column.value : undefined, style=`width: ${column.width || 160}px; height: ${column.height || 100}px`, g-item-cid=column.value === 'thumbnail' ? item.cid : undefined) - - var imageName = column.value === 'thumbnail' ? column.value : `images/${column.value}`; - img.waiting(deferred-src=`${apiRoot}/item/${item.id}/tiles/${imageName}?width=${column.width || 160}&height=${column.height || 100}`) - else if column.type === 'metadata' +
+ + + + if checkboxes + + for column, colidx in itemList.columns + if column.type !== 'image' || hasAnyLargeImage + if column.title !== undefined + - colNames[colidx] = column.title + else + - colNames[colidx] = `${column.value.substr(0, 1).toUpperCase()}${column.value.substr(1)}` + + + + + each item in items + + if checkboxes + + for column, colidx in itemList.columns + if column.type !== 'image' || hasAnyLargeImage - - let value = item.get('meta') || {} - column.value.split('.').forEach((part) => { - value = (value || {})[part]; - }) - if column.edit && accessLevel >= AccessType.WRITE - - if ((value === '' || value === undefined) && column.default) { value = column.default; } - span.large_image_metadata.lientry_edit(column-idx=colidx) - if column.enum - select.input-sm.form-control.g-widget-metadata-value-input.g-widget-metadata-lientry(title=column.description) - for enumval in column.enum - option(value=enumval, selected=('' + enumval) === ('' + value) ? 'selected' : null) - = enumval - else - input.input-sm.form-control.g-widget-metadata-value-input.g-widget-metadata-column(placeholder=column.description || "Value", value=value, title=column.description) - else - span.large_image_metadata - = value - if value - span.li-item-list-cell-filter(title="Only show items that match this metadata value exactly", filter-value=value, column-value=column.value) - i.icon-filter - if (hasMore && !paginated) - li.g-show-more - a.g-show-more-items - i.icon-level-down - | Show more items... + var divtype = column.type !== 'record' || column.value !== 'controls' ? 'a' : 'span'; + var classes = divtype == 'a' ? ['g-item-list-link']: []; + if (('' + column.type + column.value).match(/^[a-zA-Z][a-zA-Z0-9-_]*$/)) classes.push(`li-column-${column.type}-${column.value}`); + if (('' + column.type).match(/^[a-zA-Z][a-zA-Z0-9-_]*$/)) classes.push(`li-column-${column.type}`); + var skip = false; + (column.only || []).forEach((only) => { + if (!(only || {}).match) { + return; + } + var onlyval = (only.type === 'record' && only.value === 'name') ? item.name() : (only.type === 'record' && only.value === 'description') ? item.get(only.value) : ''; + if (only.type === 'metadata') { + onlyval = item.get('meta') || {}; + only.value.split('.').forEach((part) => { + onlyval = (onlyval || {})[part]; + }) + } + if (onlyval.match(new RegExp(only.match, 'i')) === null) { + skip = true; + } + }); + + + + + + + + + +
+ +
+ + = colNames[colidx] + +
+
+ + + <#{divtype} class="li-item-list-cell #{classes.join(' ')} flex items-left mx-4" g-item-cid="#{item.cid}" href="#item/#{item.id}" title="#{colNames[colidx]}"> + //-
+ if !skip && column.label + = column.label + if skip + //- ignore + else if column.type === 'record' + if column.value === 'name' + = item.name() + else if column.value === 'controls' +
+ if downloadLinks +
+ + + +
+ if viewLinks +
+ + + +
+
+ else if column.value === 'size' + = formatSize(item.get('size')) + else if column.value === 'description' + = item.get(column.value) + else if column.type === 'image' && item.get('largeImage') + .large_image_thumbnail(extra-image=column.value !== 'thumbnail' ? column.value : undefined, style=`width: ${column.width || 160}px; height: ${column.height || 100}px`, g-item-cid=column.value === 'thumbnail' ? item.cid : undefined) + - var imageName = column.value === 'thumbnail' ? column.value : `images/${column.value}`; + img.waiting(deferred-src=`${apiRoot}/item/${item.id}/tiles/${imageName}?width=${column.width || 160}&height=${column.height || 100}`) + else if column.type === 'metadata' + - + let value = item.get('meta') || {} + column.value.split('.').forEach((part) => { + value = (value || {})[part]; + }) + if column.edit && accessLevel >= AccessType.WRITE + - if ((value === '' || value === undefined) && column.default) { value = column.default; } + span.large_image_metadata.lientry_edit(column-idx=colidx) + if column.enum + select.input-sm.form-control.g-widget-metadata-value-input.g-widget-metadata-lientry(title=column.description) + for enumval in column.enum + option(value=enumval, selected=('' + enumval) === ('' + value) ? 'selected' : null) + = enumval + else + input.input-sm.form-control.g-widget-metadata-value-input.g-widget-metadata-column(placeholder=column.description || "Value", value=value, title=column.description) + else + span.large_image_metadata + = value + if value + span.li-item-list-cell-filter(title="Only show items that match this metadata value exactly", filter-value=value, column-value=column.value) + i.icon-filter + +
+
+
+ if (hasMore && !paginated) + + + | Show more items... + + +
+
+
diff --git a/girder/girder_large_image/web_client/views/TableConfigDialog.vue b/girder/girder_large_image/web_client/views/TableConfigDialog.vue new file mode 100644 index 000000000..8bd91d659 --- /dev/null +++ b/girder/girder_large_image/web_client/views/TableConfigDialog.vue @@ -0,0 +1,131 @@ + + + diff --git a/girder/girder_large_image/web_client/views/TableViewSelect.vue b/girder/girder_large_image/web_client/views/TableViewSelect.vue new file mode 100644 index 000000000..526a7d3d4 --- /dev/null +++ b/girder/girder_large_image/web_client/views/TableViewSelect.vue @@ -0,0 +1,85 @@ + + + diff --git a/girder/girder_large_image/web_client/views/configView.js b/girder/girder_large_image/web_client/views/configView.js index 078315957..5a8cebebe 100644 --- a/girder/girder_large_image/web_client/views/configView.js +++ b/girder/girder_large_image/web_client/views/configView.js @@ -278,6 +278,38 @@ var ConfigView = View.extend({ return ConfigView._liconfigSettingsRequest; }, + /** + * Save the folder config file for the current user. + * + * @param {string} folderId the folder to get the config for. + * @param {Object} content the content of the config as an Object. + * @param {function} callback a function to call after the config file is + * saved. + * @returns a promise that resolves to the config file values. + */ + saveConfigFile: function (folderId, content, callback) { + const userContent = JSON.parse(JSON.stringify(content)); + + // Only save the editable views (e.g. the ones the user created) + userContent.namedItemLists = {}; + for (const view in content.namedItemLists) { + if (content.namedItemLists[view].edit) { + userContent.namedItemLists[view] = content.namedItemLists[view]; + } + } + + restRequest({ + method: 'PUT', + url: `folder/${folderId}/yaml_config/.large_image_config.yaml?user_context=true`, + data: JSON.stringify(userContent, null, 2), + contentType: 'application/json', + }).done(() => { + if (callback) { + callback(); + } + }); + }, + /** * Clear the settings so that getSettings will refetch them. */ diff --git a/girder/girder_large_image/web_client/views/itemList.js b/girder/girder_large_image/web_client/views/itemList.js index b1f36788a..bf1807881 100644 --- a/girder/girder_large_image/web_client/views/itemList.js +++ b/girder/girder_large_image/web_client/views/itemList.js @@ -18,11 +18,21 @@ import '../stylesheets/itemList.styl'; import ItemListTemplate from '../templates/itemList.pug'; import {MetadatumWidget, validateMetadataValue} from './metadataWidget'; +import Vue from "vue"; +import TableConfigDialog from './TableConfigDialog.vue'; +import TableViewSelect from './TableViewSelect.vue'; + wrap(HierarchyWidget, 'render', function (render) { render.call(this); if (!this.$('#flattenitemlist').length && this.$('.g-item-list-container').length && this.itemListView && this.itemListView.setFlatten) { - $('button.g-checked-actions-button').parent().after( - '
' + $('.g-checked-actions').after(` +
+
+ Flatten +
+ + Flatten +
` ); if ((this.itemListView || {})._recurse) { this.$('#flattenitemlist').prop('checked', true); @@ -33,9 +43,9 @@ wrap(HierarchyWidget, 'render', function (render) { this.delegateEvents(); } if (this.$('#flattenitemlist').length && this.parentModel.get('_modelType') !== 'folder') { - this.$('.li-flatten-item-list').addClass('hidden'); + this.$('.li-flatten-item-list').addClass('htk-hidden'); } else { - this.$('.li-flatten-item-list').removeClass('hidden'); + this.$('.li-flatten-item-list').removeClass('htk-hidden'); } }); @@ -179,7 +189,79 @@ wrap(ItemListWidget, 'render', function (render) { } this._confList = () => { - return this._liconfig ? (this.$el.closest('.modal-dialog').length ? this._liconfig.itemListDialog : this._liconfig.itemList) : undefined; + if (!this._liconfig) { + return undefined; + } + if (this.$el.closest('.modal-dialog').length) { + return this._liconfig.itemListDialog; + } + if (this._liconfig.itemList && this._liconfig.itemList.fromName) { + const foundView = (this._liconfig.namedItemLists || {})[this._liconfig.itemList.fromName]; + if (foundView) { + return foundView; + } + } + return this._liconfig.itemList; + }; + + this._saveTableConfig = ({columns: config, name, newView, originalName}) => { + // Update or add the named view + if (!this._liconfig) { + this._liconfig = {}; + } + if (!this._liconfig.namedItemLists) { + this._liconfig.namedItemLists = {}; + } + if (newView) { + // Need to make the view name unique + let foundView = this._liconfig.namedItemLists[name]; + while (foundView) { + name += ' Copy'; + foundView = this._liconfig.namedItemLists[name]; + } + this._liconfig.namedItemLists[name] = {columns: config, edit: true}; + } else { + let foundView = this._liconfig.namedItemLists[originalName]; + if (foundView) { + if (originalName !== name) { + // Need to make the view name unique + let duplicate = this._liconfig.namedItemLists[name]; + while (duplicate) { + name += ' Copy'; + duplicate = this._liconfig.namedItemLists[name]; + } + delete this._liconfig.namedItemLists[originalName]; + this._liconfig.namedItemLists[name] = foundView; + } + foundView.columns = config; + } else { + // This may occur if we are moving an old existing default view to a named view + this._liconfig.namedItemLists[name] = {columns: config, edit: true}; + } + } + + // Make sure the named view is the current view + if (!this._liconfig.itemList) { + this._liconfig.itemList = {}; + } + this._liconfig.itemList.fromName = name; + delete this._liconfig.itemList.columns; + + // Save the new configuration to the yaml file + largeImageConfig.saveConfigFile(this.parentView.parentModel.id, this._liconfig, null); + itemListRender.apply(this); + }; + + this._deleteTableConfig = (name) => { + if (!this._liconfig) { + return; + } + delete this._liconfig.namedItemLists[name]; + if (this._liconfig.itemList.fromName === name) { + this._liconfig.itemList.fromName = Object.keys(this._liconfig.namedItemLists)[0] || ''; + } + itemListRender.apply(this); + largeImageConfig.saveConfigFile(this.parentView.parentModel.id, this._liconfig, null); }; /** @@ -347,25 +429,31 @@ wrap(ItemListWidget, 'render', function (render) { function itemListRender() { const root = this.$el.closest('.g-hierarchy-widget'); if (!root.find('.li-item-list-filter').length) { - let base = root.find('.g-hierarchy-actions-header .g-folder-header-buttons').eq(0); + let base = root.find('.g-checked-actions').eq(0); let func = 'after'; if (!base.length) { base = root.find('.g-hierarchy-breadcrumb-bar>.breadcrumb>div').eq(0); func = 'before'; } if (base.length) { - base[func]('Filter: ' + - '' + - ''); + base[func](` +
+ +
+ + + +
+ `); + this._tableViewSelectVue = null; // We'll need to recreate this + if (this._generalFilter) { root.find('.li-item-list-filter-input').val(this._generalFilter); } @@ -384,6 +472,14 @@ wrap(ItemListWidget, 'render', function (render) { this._setSort(); return; } + const itemList = this._confList(); + if (!itemList.columns || itemList.columns.length === 0) { + itemList.columns = [ + {type: 'record', value: 'name', title: 'Name'}, + {type: 'record', value: 'size', title: 'Size'}, + {type: 'record', value: 'controls', title: 'Controls'}, + ]; + } this.$el.html(ItemListTemplate({ items: this.collection.toArray(), isParentPublic: this.public, @@ -406,6 +502,133 @@ wrap(ItemListWidget, 'render', function (render) { AccessType: AccessType })); + // Poor man's metadata field list until we get an endpoint for it + const allColumns = [ + {type: 'image', value: 'thumbnail', title: 'Thumbnail'}, + {type: 'image', value: 'label', title: 'Label'}, + {type: 'record', value: 'controls', title: 'Controls'}, + {type: 'record', value: 'name', title: 'Name'}, + {type: 'record', value: 'size', title: 'Size'}, + ]; + const allColumnsMap = {}; + this.collection.toArray().forEach((item) => { + let value = item.get('meta') || {}; + for (const key in value) { + if (!allColumnsMap[key]) { + allColumnsMap[key] = true; + allColumns.push({ + type: 'metadata', + value: key, + title: key.replace(/[^a-zA-Z0-9]+/g, ' ') + .split(' ') + .filter(word => word.length > 0) + .map(word => + word.charAt(0).toUpperCase() + word.slice(1).toLowerCase() + ) + .join(' '), + }); + } + } + }); + + const TableConfigDialogConstructor = Vue.extend(TableConfigDialog); + this._tableConfigVue = new TableConfigDialogConstructor({ + propsData: { + config: (this._confList() || {}).columns || [], + allColumns, + name: ((this._liconfig || {}).itemList || {}).fromName || '', + newView: false, + } + }); + this._tableConfigVue.$on('save', (config, name) => { + this._saveTableConfig({columns: config, name, newView: false, originalName: this._tableConfigVue.name}); + }); + this._tableConfigVue.$mount(this.parentView.$el.find('.g-edit-table-view-dialog-container')[0]); + + const currentName = (this._liconfig || {}).itemList ? this._liconfig.itemList.fromName : undefined; + const views = (this._liconfig || {}).namedItemLists || {}; + if (this._tableViewSelectVue) { + this._tableViewSelectVue.value = currentName; + this._tableViewSelectVue.views = views; + this._tableViewSelectVue.loggedIn = !!getCurrentUser(); + } else { + const TableViewSelectConstructor = Vue.extend(TableViewSelect); + this._tableViewSelectVue = new TableViewSelectConstructor({ + propsData: { + value: currentName, + views, + loggedIn: !!getCurrentUser(), + } + }); + + this._tableViewSelectVue.$on('change', (name) => { + if (!this._liconfig) { + this._liconfig = {}; + } + if (!this._liconfig.itemList) { + this._liconfig.itemList = {}; + } + this._liconfig.itemList.fromName = name; + delete this._liconfig.itemList.columns; + itemListRender.apply(this); + if (getCurrentUser()) { + largeImageConfig.saveConfigFile(this.parentView.parentModel.id, this._liconfig, null); + } + }); + + this._tableViewSelectVue.$on('edit', (name) => { + this._tableConfigVue.config = (this._liconfig.namedItemLists || {})[name].columns; + this._tableConfigVue.name = name; + this._tableConfigVue.newView = false; + this._tableConfigVue.$refs.dialog.show(); + }); + + this._tableViewSelectVue.$on('delete', (name) => { + this._deleteTableConfig(name); + }); + + this._tableViewSelectVue.$on('new', () => { + if (this._liconfig === undefined) { + this._liconfig = {}; + } + if (this._liconfig.namedItemLists === undefined) { + this._liconfig.namedItemLists = {}; + } + let name = 'View'; + let num = 1; + let foundView = this._liconfig.namedItemLists[name]; + while (foundView) { + num += 1; + name = `View ${num}`; + foundView = this._liconfig.namedItemLists[name]; + } + const columns = [ + {type: 'record', value: 'name'}, + {type: 'record', value: 'size'}, + {type: 'record', value: 'controls'}, + ]; + this._liconfig.namedItemLists[name] = {columns, edit: true}; + + this._tableConfigVue.config = columns; + this._tableConfigVue.name = name; + this._tableConfigVue.newView = true; + this._tableConfigVue.$refs.dialog.show(); + }); + + this._tableViewSelectVue.$on('copy', (name) => { + if (this._liconfig === undefined) { + this._liconfig = {}; + } + if (this._liconfig.namedItemLists === undefined) { + this._liconfig.namedItemLists = {}; + } + let foundView = this._liconfig.namedItemLists[name]; + this._saveTableConfig({columns: foundView.columns, name, newView: true}); + }); + + this._tableViewSelectVue.$mount(this.parentView.$el.find('.g-table-view-select')[0]); + } + const parent = this.$el; this.$el.find('.large_image_thumbnail').each(function () { var elem = $(this); From 2a706edd06e692e0f8a00e1d4a5c40e545e1fd8d Mon Sep 17 00:00:00 2001 From: Jeffrey Baumes Date: Thu, 26 Sep 2024 15:43:31 -0400 Subject: [PATCH 008/103] Linting fixes --- .../web_client/package.json | 1 + .../web_client/views/TableConfigDialog.vue | 176 ++++++++++++------ .../web_client/views/TableViewSelect.vue | 93 ++++++--- .../web_client/views/configView.js | 2 +- .../web_client/views/itemList.js | 24 +-- 5 files changed, 192 insertions(+), 104 deletions(-) diff --git a/girder/girder_large_image/web_client/package.json b/girder/girder_large_image/web_client/package.json index b208d1ed7..27e979dc4 100644 --- a/girder/girder_large_image/web_client/package.json +++ b/girder/girder_large_image/web_client/package.json @@ -77,6 +77,7 @@ ], "pugLintConfig": { "extends": "@girder/pug-lint-config", + "disallowHtmlText": null, "excludeFiles": [ "**/node_modules/" ] diff --git a/girder/girder_large_image/web_client/views/TableConfigDialog.vue b/girder/girder_large_image/web_client/views/TableConfigDialog.vue index 8bd91d659..f1693cd41 100644 --- a/girder/girder_large_image/web_client/views/TableConfigDialog.vue +++ b/girder/girder_large_image/web_client/views/TableConfigDialog.vue @@ -1,85 +1,139 @@ diff --git a/girder/girder_large_image/web_client/views/TableViewSelect.vue b/girder/girder_large_image/web_client/views/TableViewSelect.vue index 526a7d3d4..48c0165bc 100644 --- a/girder/girder_large_image/web_client/views/TableViewSelect.vue +++ b/girder/girder_large_image/web_client/views/TableViewSelect.vue @@ -1,32 +1,65 @@ diff --git a/girder/girder_large_image/web_client/views/configView.js b/girder/girder_large_image/web_client/views/configView.js index 5a8cebebe..7e4afe222 100644 --- a/girder/girder_large_image/web_client/views/configView.js +++ b/girder/girder_large_image/web_client/views/configView.js @@ -302,7 +302,7 @@ var ConfigView = View.extend({ method: 'PUT', url: `folder/${folderId}/yaml_config/.large_image_config.yaml?user_context=true`, data: JSON.stringify(userContent, null, 2), - contentType: 'application/json', + contentType: 'application/json' }).done(() => { if (callback) { callback(); diff --git a/girder/girder_large_image/web_client/views/itemList.js b/girder/girder_large_image/web_client/views/itemList.js index bf1807881..a3f743460 100644 --- a/girder/girder_large_image/web_client/views/itemList.js +++ b/girder/girder_large_image/web_client/views/itemList.js @@ -1,6 +1,7 @@ import $ from 'jquery'; import _ from 'underscore'; import Backbone from 'backbone'; +import Vue from 'vue'; import {wrap} from '@girder/core/utilities/PluginUtils'; import {getApiRoot} from '@girder/core/rest'; @@ -18,7 +19,6 @@ import '../stylesheets/itemList.styl'; import ItemListTemplate from '../templates/itemList.pug'; import {MetadatumWidget, validateMetadataValue} from './metadataWidget'; -import Vue from "vue"; import TableConfigDialog from './TableConfigDialog.vue'; import TableViewSelect from './TableViewSelect.vue'; @@ -221,7 +221,7 @@ wrap(ItemListWidget, 'render', function (render) { } this._liconfig.namedItemLists[name] = {columns: config, edit: true}; } else { - let foundView = this._liconfig.namedItemLists[originalName]; + const foundView = this._liconfig.namedItemLists[originalName]; if (foundView) { if (originalName !== name) { // Need to make the view name unique @@ -477,7 +477,7 @@ wrap(ItemListWidget, 'render', function (render) { itemList.columns = [ {type: 'record', value: 'name', title: 'Name'}, {type: 'record', value: 'size', title: 'Size'}, - {type: 'record', value: 'controls', title: 'Controls'}, + {type: 'record', value: 'controls', title: 'Controls'} ]; } this.$el.html(ItemListTemplate({ @@ -508,11 +508,11 @@ wrap(ItemListWidget, 'render', function (render) { {type: 'image', value: 'label', title: 'Label'}, {type: 'record', value: 'controls', title: 'Controls'}, {type: 'record', value: 'name', title: 'Name'}, - {type: 'record', value: 'size', title: 'Size'}, + {type: 'record', value: 'size', title: 'Size'} ]; const allColumnsMap = {}; this.collection.toArray().forEach((item) => { - let value = item.get('meta') || {}; + const value = item.get('meta') || {}; for (const key in value) { if (!allColumnsMap[key]) { allColumnsMap[key] = true; @@ -521,11 +521,11 @@ wrap(ItemListWidget, 'render', function (render) { value: key, title: key.replace(/[^a-zA-Z0-9]+/g, ' ') .split(' ') - .filter(word => word.length > 0) - .map(word => + .filter((word) => word.length > 0) + .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase() ) - .join(' '), + .join(' ') }); } } @@ -537,7 +537,7 @@ wrap(ItemListWidget, 'render', function (render) { config: (this._confList() || {}).columns || [], allColumns, name: ((this._liconfig || {}).itemList || {}).fromName || '', - newView: false, + newView: false } }); this._tableConfigVue.$on('save', (config, name) => { @@ -557,7 +557,7 @@ wrap(ItemListWidget, 'render', function (render) { propsData: { value: currentName, views, - loggedIn: !!getCurrentUser(), + loggedIn: !!getCurrentUser() } }); @@ -605,7 +605,7 @@ wrap(ItemListWidget, 'render', function (render) { const columns = [ {type: 'record', value: 'name'}, {type: 'record', value: 'size'}, - {type: 'record', value: 'controls'}, + {type: 'record', value: 'controls'} ]; this._liconfig.namedItemLists[name] = {columns, edit: true}; @@ -622,7 +622,7 @@ wrap(ItemListWidget, 'render', function (render) { if (this._liconfig.namedItemLists === undefined) { this._liconfig.namedItemLists = {}; } - let foundView = this._liconfig.namedItemLists[name]; + const foundView = this._liconfig.namedItemLists[name]; this._saveTableConfig({columns: foundView.columns, name, newView: true}); }); From 133de0c2628b853f8b8a7371b3af87bb79c6baab Mon Sep 17 00:00:00 2001 From: David Manthey Date: Fri, 27 Sep 2024 08:55:58 -0400 Subject: [PATCH 009/103] Reduce updates when showing item lists; add a waiting spinner When loading long pages with sorts, sometimes the initial page number shown in the bottom pagination bar was wrong. This fixes that issue. --- CHANGELOG.md | 1 + .../web_client/views/itemList.js | 23 +++++++++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eff20b494..c38c7d7a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Better handle images without enough tile layers ([#1648](../../pull/1648)) - Add users option to config files; have a default config file ([#1649](../../pull/1649)) - Remove no longer used code; adjust item list slightly ([#1651](../../pull/1651)) +- Reduce updates when showing item lists; add a waiting spinner ([#1653](../../pull/1653)) ### Bug Fixes diff --git a/girder/girder_large_image/web_client/views/itemList.js b/girder/girder_large_image/web_client/views/itemList.js index 4f659dff3..22c187d0c 100644 --- a/girder/girder_large_image/web_client/views/itemList.js +++ b/girder/girder_large_image/web_client/views/itemList.js @@ -46,6 +46,7 @@ wrap(FolderListWidget, 'checkAll', function (checkAll, checked) { }); wrap(ItemListWidget, 'initialize', function (initialize, settings) { + this._inInit = true; const result = initialize.call(this, settings); delete this._hasAnyLargeImage; @@ -54,6 +55,8 @@ wrap(ItemListWidget, 'initialize', function (initialize, settings) { this._liconfig = val; } if (_.isEqual(val, this._liconfig) && !this._recurse) { + this._inInit = false; + this.render(); return; } delete this._lastSort; @@ -74,12 +77,14 @@ wrap(ItemListWidget, 'initialize', function (initialize, settings) { update = true; } else if (this._confList() && this._confList().defaultSort && this._confList().defaultSort.length) { this._lastSort = this._confList().defaultSort; + update = true; } if (query.filter || this._recurse) { this._generalFilter = query.filter; this._setFilter(false); update = true; } + this._inInit = false; if (update) { this._setSort(); } else { @@ -104,6 +109,9 @@ wrap(ItemListWidget, 'initialize', function (initialize, settings) { wrap(ItemListWidget, 'render', function (render) { this.$el.closest('.modal-dialog').addClass('li-item-list-dialog'); + if (!this.$el.children().length) { + this.$el.html(''); + } /* Chrome limits the number of connections to a single domain, which means * that time-consuming requests for thumbnails can bind-up the web browser. @@ -155,12 +163,16 @@ wrap(ItemListWidget, 'render', function (render) { const pages = Math.ceil(this.collection.getTotalCount() / this.collection.pageLimit); this._totalPages = pages; this._inFetch = false; - if (this._needsFetch) { - this._setSort(); - } - if (oldPages !== pages) { + itemListRender.apply(this, _.rest(arguments)); + if (oldPages !== pages || this.collection.offset !== this.collection.size()) { + this.collection.offset = this.collection.size(); this.trigger('g:paginated'); this.collection.trigger('g:changed'); + } else { + itemListRender.apply(this, _.rest(arguments)); + } + if (this._needsFetch) { + this._setSort(); } }); } else { @@ -300,6 +312,9 @@ wrap(ItemListWidget, 'render', function (render) { }; function itemListRender() { + if (this._inInit || this._inFetch) { + return; + } const root = this.$el.closest('.g-hierarchy-widget'); if (!root.find('.li-item-list-filter').length) { let base = root.find('.g-hierarchy-actions-header .g-folder-header-buttons').eq(0); From a00e885c26bccf0ff6ffe4b0da1e171833337442 Mon Sep 17 00:00:00 2001 From: Jeffrey Baumes Date: Fri, 27 Sep 2024 10:30:02 -0400 Subject: [PATCH 010/103] Enable configuration of the list of columns that are allowed to be used in itemList views --- .../web_client/views/configView.js | 2 + .../web_client/views/itemList.js | 65 ++++++++++--------- 2 files changed, 37 insertions(+), 30 deletions(-) diff --git a/girder/girder_large_image/web_client/views/configView.js b/girder/girder_large_image/web_client/views/configView.js index 7e4afe222..9a5566eb9 100644 --- a/girder/girder_large_image/web_client/views/configView.js +++ b/girder/girder_large_image/web_client/views/configView.js @@ -297,6 +297,8 @@ var ConfigView = View.extend({ userContent.namedItemLists[view] = content.namedItemLists[view]; } } + // User should not set the columns that can be used + delete userContent.allColumns; restRequest({ method: 'PUT', diff --git a/girder/girder_large_image/web_client/views/itemList.js b/girder/girder_large_image/web_client/views/itemList.js index a3f743460..6c1c3458b 100644 --- a/girder/girder_large_image/web_client/views/itemList.js +++ b/girder/girder_large_image/web_client/views/itemList.js @@ -264,6 +264,40 @@ wrap(ItemListWidget, 'render', function (render) { largeImageConfig.saveConfigFile(this.parentView.parentModel.id, this._liconfig, null); }; + this._allColumns = () => { + if (this._liconfig && this._liconfig.allColumns) { + return this._liconfig.allColumns; + } + const allColumns = [ + {type: 'image', value: 'thumbnail', title: 'Thumbnail'}, + {type: 'image', value: 'label', title: 'Label'}, + {type: 'record', value: 'controls', title: 'Controls'}, + {type: 'record', value: 'name', title: 'Name'}, + {type: 'record', value: 'size', title: 'Size'} + ]; + const allColumnsMap = {}; + this.collection.toArray().forEach((item) => { + const value = item.get('meta') || {}; + for (const key in value) { + if (!allColumnsMap[key]) { + allColumnsMap[key] = true; + allColumns.push({ + type: 'metadata', + value: key, + title: key.replace(/[^a-zA-Z0-9]+/g, ' ') + .split(' ') + .filter((word) => word.length > 0) + .map((word) => + word.charAt(0).toUpperCase() + word.slice(1).toLowerCase() + ) + .join(' ') + }); + } + } + }); + return allColumns; + }; + /** * Set sort on the collection and perform a debounced re-fetch. */ @@ -502,40 +536,11 @@ wrap(ItemListWidget, 'render', function (render) { AccessType: AccessType })); - // Poor man's metadata field list until we get an endpoint for it - const allColumns = [ - {type: 'image', value: 'thumbnail', title: 'Thumbnail'}, - {type: 'image', value: 'label', title: 'Label'}, - {type: 'record', value: 'controls', title: 'Controls'}, - {type: 'record', value: 'name', title: 'Name'}, - {type: 'record', value: 'size', title: 'Size'} - ]; - const allColumnsMap = {}; - this.collection.toArray().forEach((item) => { - const value = item.get('meta') || {}; - for (const key in value) { - if (!allColumnsMap[key]) { - allColumnsMap[key] = true; - allColumns.push({ - type: 'metadata', - value: key, - title: key.replace(/[^a-zA-Z0-9]+/g, ' ') - .split(' ') - .filter((word) => word.length > 0) - .map((word) => - word.charAt(0).toUpperCase() + word.slice(1).toLowerCase() - ) - .join(' ') - }); - } - } - }); - const TableConfigDialogConstructor = Vue.extend(TableConfigDialog); this._tableConfigVue = new TableConfigDialogConstructor({ propsData: { config: (this._confList() || {}).columns || [], - allColumns, + allColumns: this._allColumns(), name: ((this._liconfig || {}).itemList || {}).fromName || '', newView: false } From d2d95107793d98cfcbaa923b9ee13a4f7e9b9c8d Mon Sep 17 00:00:00 2001 From: Jeffrey Baumes Date: Fri, 27 Sep 2024 11:56:32 -0400 Subject: [PATCH 011/103] Better help on hover for table search --- .../web_client/views/TableViewSelect.vue | 5 ++++- .../girder_large_image/web_client/views/itemList.js | 11 +++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/girder/girder_large_image/web_client/views/TableViewSelect.vue b/girder/girder_large_image/web_client/views/TableViewSelect.vue index 48c0165bc..1bf320fe6 100644 --- a/girder/girder_large_image/web_client/views/TableViewSelect.vue +++ b/girder/girder_large_image/web_client/views/TableViewSelect.vue @@ -1,8 +1,11 @@