diff --git a/.github/badges/code_issues.svg b/.github/badges/code_issues.svg
index 9d1b92d3..a0288219 100644
--- a/.github/badges/code_issues.svg
+++ b/.github/badges/code_issues.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/code/+nansen/+config/+dloc/@DataLocationModel/DataLocationModel.m b/code/+nansen/+config/+dloc/@DataLocationModel/DataLocationModel.m
index 2df68acb..7c74e8f8 100644
--- a/code/+nansen/+config/+dloc/@DataLocationModel/DataLocationModel.m
+++ b/code/+nansen/+config/+dloc/@DataLocationModel/DataLocationModel.m
@@ -1,19 +1,19 @@
classdef DataLocationModel < utility.data.StorableCatalog
%DataLocationModel Interface for detecting path of data/session folders
-
+
% TODOS:
% QUESTIONS:
-
+
properties (Constant, Hidden)
ITEM_TYPE = 'Data Location'
end
-
+
properties (Dependent, SetAccess = private)
DataLocationNames
NumDataLocations
end
-
+
properties (Dependent)
IsDirty % Todo: Dependent on whether data backup is different than data
DefaultDataLocation
@@ -23,23 +23,23 @@
DataBackup % Todo: assign this on construction and when model is marked as clean(?)
LocalRootPathManager nansen.config.dloc.LocalRootPathManager
end
-
+
events
DataLocationAdded
DataLocationModified
DataLocationRemoved
end
-
+
methods (Static) % Methods in separate files
- %S = getEmptyItem()
-
+ % S = getEmptyItem()
+
S = getBlankItem()
-
+
S = getDefaultItem()
end
methods (Static)
-
+
% function S = getEmptyObject()
%
% import nansen.config.dloc.DataLocationModel
@@ -56,19 +56,20 @@
% end
function S = getDefaultMetadataStructure()
-
+
varNames = {'Subject ID', 'Session ID', 'Experiment Date', 'Experiment Time'};
numVars = numel(varNames);
-
+
S = struct(...
'VariableName', varNames, ...
'SubfolderLevel', {[]}, ...
'StringDetectMode', repmat({'ind'}, 1, numVars), ...
'StringDetectInput', repmat({'1:end'}, 1, numVars), ...
'StringFormat', repmat({''}, 1, numVars), ...
+ 'Separator', repmat({''}, 1, numVars), ...
'FunctionName', repmat({''}, 1, numVars) );
end
-
+
function S = getDefaultSubfolderStructure()
%getDefaultSubfolderStructure Create a default struct
S = struct(...
@@ -80,59 +81,59 @@
% Todo: Add ShortName, i.e sub / ses (ref BIDS)
end
end
-
+
methods % Constructor
function obj = DataLocationModel(varargin)
% Superclass constructor. Loads given (or default) archive
obj@utility.data.StorableCatalog(varargin{:})
-
+
% Initialize the local root path manager
obj.initializeLocalRootPathManager();
-
+
obj.updateMetadataExtractorFunctionNames()
obj.tempDevFix()
end
-
+
function tempDevFix(obj)
-
+
dirty = false;
-
+
% Add default data location to preferences
if ~isfield(obj.Preferences, 'DefaultDataLocation')
obj.fixDefaultDataLocation()
dirty = true;
end
-
+
% Rootpath field changed from cell array with 2 cells to root
% array with single to multiple cells ( remove empty cell(s) )
for i = 1:numel(obj.Data)
rootPath = obj.Data(i).RootPath;
-
+
if any(strcmp(rootPath, ''))
rootPath(strcmp(rootPath, '')) = [];
dirty = true;
end
-
+
obj.Data(i).RootPath = rootPath;
end
-
+
% Add 'Type' as a table variable on the third column
if ~isfield(obj.Data, 'Type')
obj.addTypeAsTableVariable()
dirty = true;
end
-
+
% Reorder so that Type is the third table variable
fieldNames = fieldnames(obj.Data);
if ~strcmp(fieldNames{3}, 'Type')
fieldNamesNew = setdiff(fieldNames, 'Type', 'stable');
-
+
obj.Data = orderfields(obj.Data, ...
[fieldNamesNew(1:2); 'Type'; fieldNamesNew(3:end)]);
-
+
dirty = true;
end
-
+
if ~isfield(obj.Preferences, 'SourceID')
obj.Preferences.SourceID = utility.system.getComputerName(true);
dirty = true;
@@ -186,51 +187,50 @@ function tempDevFix(obj)
end
end
end
-
+
methods % Set/get methods
-
+
function numDataLocations = get.NumDataLocations(obj)
numDataLocations = numel(obj.Data);
end
-
+
function dataLocationNames = get.DataLocationNames(obj)
dataLocationNames = obj.ItemNames;
end
-
+
function defaultDataLocation = get.DefaultDataLocation(obj)
-
+
if isempty(obj.Data); defaultDataLocation = ''; return; end
-
+
dataLocationUuid = obj.Preferences.DefaultDataLocation;
defaultDataLocation = obj.getNameFromUuid(dataLocationUuid);
-
end
-
+
function set.DefaultDataLocation(obj, newValue)
-
+
assert(ischar(newValue), 'Please provide a character vector with the name of a data location')
-
+
% Check if data location with given name exists...
message = sprintf('"%s" can not be a default data location because no data location with this name exists.', newValue);
assert(any(strcmp(obj.DataLocationNames, newValue)), message)
-
+
% Check if data location is allowed to be a default data location.
dataLocationItem = obj.getDataLocation(newValue);
message = sprintf('"%s" can not be a default data location because the data location is of type "%s".', newValue, dataLocationItem.Type.Name);
assert(dataLocationItem.Type.AllowAsDefault, message)
-
+
dataLocationUuid = dataLocationItem.Uuid;
-
+
obj.Preferences.DefaultDataLocation = dataLocationUuid;
end
end
-
+
methods % Defined in separate files
dataFolders = listDataFolders(obj, dataLocationName, options)
end
methods % Modify save/load to include local settings...
-
+
% % function load(obj)
% %
% % load@utility.data.StorableCatalog(obj)
@@ -241,11 +241,10 @@ function tempDevFix(obj)
% %
% % end
% %
-
end
-
+
methods
-
+
function configureLocalRootpath(obj, localRootPath, originalRootPath)
obj.LocalRootPathManager.configureLocalRootPath(localRootPath, originalRootPath)
obj.load()
@@ -253,7 +252,7 @@ function configureLocalRootpath(obj, localRootPath, originalRootPath)
function validateRootPath(obj, dataLocIdx)
%validateRootPath Check if root path exists
-
+
% Todo: Loop through all entries in cell array (if many are present)
thisDataLoc = obj.Data(dataLocIdx);
@@ -262,34 +261,34 @@ function validateRootPath(obj, dataLocIdx)
error('Root path for DataLocation "%s" does not exist', thisName)
end
end
-
+
function createRootPath(obj, dataLocIdx, rootIdx)
-
+
if nargin < 3; rootIdx = 1; end
thisRootPath = obj.Data(dataLocIdx).RootPath(rootIdx).Value;
-
+
if ~isfolder(thisRootPath)
mkdir(thisRootPath)
fprintf('Created root directory for DataLocation %s\n', obj.Data(dataLocIdx).Name)
end
end
-
+
function diskName = resolveDiskName(obj, rootPath)
diskName = obj.LocalRootPathManager.resolveDiskName(rootPath);
end
end
-
+
methods % Methods for updating substructs of data location
-
+
function updateMetaDataDefinitions(obj, newStruct, dataLocIdx)
%updateMetaDataDefinitions Update the metadata definition struct
%
% Just replaces the struct in the MetaDataDef property with the
% input struct S.
-
+
oldStruct = obj.Data(dataLocIdx).MetaDataDef;
obj.Data(dataLocIdx).MetaDataDef = newStruct;
-
+
% Trigger ModelChanged event
evtData = uiw.event.EventData('DataLocationIndex', dataLocIdx, ...
'SubField', 'MetadataDefiniton', 'OldData', oldStruct, ...
@@ -298,33 +297,33 @@ function updateMetaDataDefinitions(obj, newStruct, dataLocIdx)
% % % Not needed at the moment
% % % obj.notify('DataLocationModified', evtData)
end
-
+
function updateSubfolderStructure(obj, newStruct, idx)
%updateSubfolderStructure Update the SubfolderStructure struct
%
% Just replaces the struct in the SubfolderStructure property
% with the input struct S.
-
+
if nargin < 3
idx = 1;
end
-
+
dataLocationName = obj.Data(idx).Name;
-
+
obj.modifyDataLocation(dataLocationName, ...
'SubfolderStructure', newStruct)
-
- %oldStruct = obj.Data(idx).SubfolderStructure;
- %obj.Data(idx).SubfolderStructure = newStruct;
-
+
+ % oldStruct = obj.Data(idx).SubfolderStructure;
+ % obj.Data(idx).SubfolderStructure = newStruct;
+
% Update example path
subFolderNames = {newStruct.Name};
-
+
if ~isempty(obj.Data(idx).RootPath)
obj.Data(idx).ExamplePath = ...
fullfile(obj.Data(idx).RootPath(1).Value, subFolderNames{:});
end
-
+
% % % Trigger ModelChanged event
% % evtData = uiw.event.EventData('DataLocationIndex', idx, ...
% % 'SubField', 'SubfolderStructure', 'OldData', oldStruct, ...
@@ -332,7 +331,7 @@ function updateSubfolderStructure(obj, newStruct, idx)
% %
% % obj.notify('DataLocationModified', evtData)
end
-
+
function dataLocationStructArray = validateDataLocationPaths(obj, dataLocationStructArray)
%validateSubfolders Validate subfolders of data locations
%
@@ -344,21 +343,21 @@ function updateSubfolderStructure(obj, newStruct, idx)
% the operating system
% % Todo: Consolidate with session/fixDataLocations
-
+
if isempty(dataLocationStructArray); return; end
-
+
if isa(dataLocationStructArray, 'cell')
dataLocationStructArray = utility.struct.structcat(1, dataLocationStructArray{:});
end
-
+
if ~isfield(dataLocationStructArray, 'Subfolders'); return; end
-
+
% Assume all subfolders are equal...
-
+
[numItems, numDatalocations] = size(dataLocationStructArray);
-
+
for i = 1:numDatalocations
-
+
dlUuid = dataLocationStructArray(1,i).Uuid;
dlInfo = obj.getItem(dlUuid);
@@ -367,10 +366,10 @@ function updateSubfolderStructure(obj, newStruct, idx)
% Update the root directory from the model
rootUid = dataLocationStructArray(j, i).RootUid;
rootIdx = find( strcmp( {dlInfo.RootPath.Key}, rootUid ) );
-
+
if ~isempty(rootIdx)
rootPathStr = dlInfo.RootPath(rootIdx).Value;
-
+
if ispc
% Todo: % Assign correct drive letter.
% Check and assign correct drive letter
@@ -384,7 +383,7 @@ function updateSubfolderStructure(obj, newStruct, idx)
end
dataLocationStructArray(j, i).RootIdx = rootIdx;
dataLocationStructArray(j, i).Diskname = diskName;
-
+
% Make sure file separators match the file system.
iSubfolder = dataLocationStructArray(j,i).Subfolders;
if isempty(iSubfolder)
@@ -398,25 +397,25 @@ function updateSubfolderStructure(obj, newStruct, idx)
end
end
end
-
+
function updateVolumeInfo(obj)
%updateVolumeInfo Update the volume info table
obj.LocalRootPathManager.updateVolumeInfo();
obj.Data = obj.LocalRootPathManager.updateRootPathFromDiskName(obj.Data);
end
end
-
+
methods % Methods for accessing/modifying items
-
+
function addDataLocation(obj, newDataLocation)
%addDataLocation Add data location item to data
-
+
if isempty(newDataLocation.Name)
newDataLocation.Name = obj.getNewName();
end
-
+
newDataLocation = obj.insertItem(newDataLocation);
-
+
% Trigger DataLocationAdded event
evtData = uiw.event.EventData(...
'NewValue', newDataLocation);
@@ -445,15 +444,15 @@ function addDataLocationFromTemplateName(obj, templateName, options)
options.Type (1,1) string = missing
options.RootDirectory (1,1) string = missing
end
-
+
% Will look for template in project and included modules
project = nansen.getCurrentProject();
templates = project.getTable('DataLocations');
-
+
isMatch = templates.Name == templateName;
if any(isMatch)
dataLocation = table2struct(templates(isMatch, :));
-
+
if ~ismissing(options.Name)
dataLocation.Name = char(options.Name);
end
@@ -471,7 +470,7 @@ function addDataLocationFromTemplateName(obj, templateName, options)
end
dataLocation.ExamplePath = '';
dataLocation.DataSubfolders = {};
-
+
% Remove template props
dataLocation = rmfield(dataLocation, ["x_type", "x_version", "DataType"]);
@@ -482,27 +481,26 @@ function addDataLocationFromTemplateName(obj, templateName, options)
end
end
-
function removeDataLocation(obj, dataLocationName)
%removeDataLocation Remove data location item from data
-
+
% Todo: Necessary if a undo operation is implemented...
- %oldValue = obj.getItem(dataLocationName);
-
+ % oldValue = obj.getItem(dataLocationName);
+
[~, idx] = obj.containsItem(dataLocationName);
-
+
obj.removeItem(dataLocationName)
-
+
% Todo: Unset default data location if this was the default
% data location
-
+
% Trigger ModelChanged event
evtData = uiw.event.EventData(...
'DataLocationIndex', idx, ...
'DataLocationName', dataLocationName);
obj.notify('DataLocationRemoved', evtData)
end
-
+
function modifyDataLocation(obj, dataLocationName, field, value)
%modifyDataLocation Change data field of DataLocation
%
@@ -511,23 +509,23 @@ function modifyDataLocation(obj, dataLocationName, field, value)
% dataLocationName is the name of the data location to modify. If
% the modification is on the name itself, the dataLocationName
% should be the current (old) name.
-
+
[tf, idx] = obj.containsItem(dataLocationName);
-
+
if ~any(tf)
error('DataLocation with name "%s" does not exist', dataLocationName)
end
-
+
% Make sure data location type is one of the type enumeration
% members:
if strcmp(field, 'Type') && ischar(value)
value = nansen.config.dloc.DataLocationType(value);
% Todo: Make sure default datalocation is still allowed type:
end
-
+
oldValue = obj.Data(idx).(field);
obj.Data(idx).(field) = value;
-
+
if strcmp(field, 'Name') % Special case if name is changed
obj.onDataLocationRenamed(dataLocationName, value)
dataLocationName = value;
@@ -539,52 +537,54 @@ function modifyDataLocation(obj, dataLocationName, field, value)
'DataField', field, ...
'NewValue', value, ...
'OldValue', oldValue);
-
+
obj.notify('DataLocationModified', evtData)
end
-
+
function dataLocationItem = getDefaultDataLocation(obj)
%getDefaultDataLocation Get the default datalocation item
dataLocationName = obj.DefaultDataLocation;
dataLocationItem = obj.getDataLocation(dataLocationName);
end
-
+
function S = getDataLocation(obj, dataLocationName)
%getDataLocation Get datalocation item by name
S = obj.getItem(dataLocationName);
end
-
+
function pathStr = getExampleFolderPath(obj, dataLocationName)
-
+
dataLocation = obj.getItem(dataLocationName);
pathStr = dataLocation.ExamplePath;
end
end
-
+
methods % Methods for getting data descriptions from filepaths
-
+
function substring = getSubjectID(obj, pathStr, dataLocationIndex)
% getSubjectID - Extract subject ID from a path string
-
+
if nargin < 3 || isempty(dataLocationIndex)
dataLocationIndex = 1;
end
S = obj.getMetavariableStruct('Subject ID', dataLocationIndex);
- substring = obj.getSubstringFromFolder(pathStr, S, dataLocationIndex);
+ dataLocationName = obj.Data(dataLocationIndex).Name;
+ substring = obj.getSubstringFromFolder(pathStr, S, dataLocationName);
end
-
+
function substring = getSessionID(obj, pathStr, dataLocationIndex)
% getSessionID - Extract session ID from a path string
-
+
if nargin < 3 || isempty(dataLocationIndex)
dataLocationIndex = 1;
end
S = obj.getMetavariableStruct('Session ID', dataLocationIndex);
- substring = obj.getSubstringFromFolder(pathStr, S, dataLocationIndex);
+ dataLocationName = obj.Data(dataLocationIndex).Name;
+ substring = obj.getSubstringFromFolder(pathStr, S, dataLocationName);
end
-
+
function value = getTime(obj, pathStr, dataLocationIndex)
% getTime - Extract experiment time from a path string
@@ -593,8 +593,9 @@ function modifyDataLocation(obj, dataLocationName, field, value)
end
S = obj.getMetavariableStruct('Experiment Time', dataLocationIndex);
- substring = obj.getSubstringFromFolder(pathStr, S, dataLocationIndex);
-
+ dataLocationName = obj.Data(dataLocationIndex).Name;
+ substring = obj.getSubstringFromFolder(pathStr, S, dataLocationName);
+
% Convert to datetime type.
if isfield(S, 'StringFormat') && ~isempty(S.StringFormat)
try
@@ -608,17 +609,18 @@ function modifyDataLocation(obj, dataLocationName, field, value)
value = substring;
end
end
-
+
function value = getDate(obj, pathStr, dataLocationIndex)
% getDate - Extract experiment date from a path string
if nargin < 3 || isempty(dataLocationIndex)
dataLocationIndex = 1;
end
-
+
S = obj.getMetavariableStruct('Experiment Date', dataLocationIndex);
- substring = obj.getSubstringFromFolder(pathStr, S, dataLocationIndex);
-
+ dataLocationName = obj.Data(dataLocationIndex).Name;
+ substring = obj.getSubstringFromFolder(pathStr, S, dataLocationName);
+
% Convert to datetime type.
if isfield(S, 'StringFormat') && ~isempty(S.StringFormat)
value = datetime(substring, 'InputFormat', S.StringFormat);
@@ -627,9 +629,9 @@ function modifyDataLocation(obj, dataLocationName, field, value)
end
end
end
-
+
methods % Utility methods
-
+
function dlStruct = expandDataLocationInfo(obj, dlStruct)
%expandDataLocation Expand information of data location structure
%
@@ -638,8 +640,9 @@ function modifyDataLocation(obj, dataLocationName, field, value)
% Name : Name of datalocation
% Type : Datalocation type
% RootPath : Key, Value pair of local rootpath.
-
+
% Todo: Why is this sometimes a cell?
+
if isa(dlStruct, 'cell')
dlStruct = dlStruct{1};
warning('Data is in an unexpected format. This is not critical, but should be investigated.')
@@ -648,7 +651,7 @@ function modifyDataLocation(obj, dataLocationName, field, value)
for iDl = 1:numel(dlStruct) %obj.NumDataLocations
dlUuid = dlStruct(iDl).Uuid;
-
+
thisDlItem = obj.getItem(dlUuid);
% Add name and type fields
@@ -666,8 +669,9 @@ function modifyDataLocation(obj, dataLocationName, field, value)
end
end
end
-
+
function dlStruct = reduceDataLocationInfo(~, dlStruct)
+
fieldsToRemove = {'Name', 'Type', 'RootPath'};
for i = 1:numel(fieldsToRemove)
if isfield(dlStruct, fieldsToRemove{i})
@@ -676,116 +680,122 @@ function modifyDataLocation(obj, dataLocationName, field, value)
end
end
end
-
+
methods (Access = ?nansen.config.dloc.DataLocationModelApp)
-
+
function restore(obj, data)
obj.Data = data;
obj.save()
end
end
-
+
methods (Access = protected)
-
+
function item = validateItem(obj, item)
% Todo...
item = validateItem@utility.data.StorableCatalog(obj, item);
end
-
+
function S = getMetavariableStruct(obj, varName, dataLocationIdx)
%getMetavariableStruct Get metadata struct for given variable
%
% Get struct containing instructions for how to find substring
% (value of a metadata variable) from a directory path.
-
+
if nargin < 3 || isempty(dataLocationIdx)
dataLocationIdx = 1;
end
-
+
S = obj.Data(dataLocationIdx).MetaDataDef;
% Find struct entry corresponding to requested variable
variableIdx = strcmp({S.VariableName}, varName);
S = S(variableIdx);
-
+
% Need to know how many subfolders the data location has
numSubfolders = numel(obj.Data(dataLocationIdx).SubfolderStructure);
S.NumSubfolders = numSubfolders;
end
+ end
+
+ methods (Static, Hidden) % Extraction helpers — public but hidden from the API
- function substring = getSubstringFromFolder(obj, pathStr, S, dataLocationIndex)
+ function substring = getSubstringFromFolder(pathStr, S, dataLocationName)
%getSubstringFromFolder Find substring from a pathstring.
%
- % substring = getSubstringFromFolder(obj, pathStr, varName) Get a
- % substring containing the value of a variable given by varName.
- % The substring is obtained from the given pathStr based on
- % instructions from the DataLocationModel's MetaDataDef property.
-
- % Initialize output
+ % substring = getSubstringFromFolder(pathStr, S, dataLocationName)
+ % extracts a metadata value from pathStr using the extraction
+ % rules in S (a MetaDataDef entry augmented with NumSubfolders).
+ %
+ % SubfolderLevel may be an array; the folder names at each level
+ % are combined with Separator before pattern extraction, so a
+ % single ind/expr rule applies to the whole combined string.
+
+ arguments
+ pathStr
+ S
+ dataLocationName
+ end
+
substring = '';
- dataLocationName = obj.Data(dataLocationIndex).Name;
mode = S.StringDetectMode;
if strcmp(mode, 'func')
- substring = feval(S.FunctionName, pathStr, dataLocationName);
- % nb: substring could be datetime value
+ try
+ substring = feval(S.FunctionName, pathStr, dataLocationName);
+ catch ME
+ switch ME.identifier
+ case 'MATLAB:UndefinedFunction'
+ error('NANSEN:DataLocationModel:FunctionNotFound', ...
+ 'The metadata extraction function "%s" was not found on the MATLAB path.', ...
+ S.FunctionName)
+ otherwise
+ rethrow(ME)
+ end
+ end
+ % nb: substring could be a datetime value
if ischar(substring) && strcmp(substring, 'N/A'); substring = ''; end
return
end
strPattern = S.StringDetectInput;
- folderLevel = S.SubfolderLevel;
-
- % Abort if instructions are not present.
- if isempty(strPattern) || isempty(folderLevel)
- return;
- end
-
- % Get the index of the folder containing the substring,
- % counting backward from the deepest subfolder level.
- reversedFolderIdx = S.NumSubfolders - folderLevel;
-
- folderNames = strsplit(pathStr, filesep);
- folderName = folderNames{end-reversedFolderIdx}; % Unpack from cell array
-
- % Get the substring using either indexing or regular
- % expressions.
- try
- switch lower(mode)
-
- case 'ind'
- %substring = folderName(strPattern);
- substring = eval( ['folderName([' strPattern '])'] );
-
- case 'expr'
- substring = regexp(folderName, strPattern, 'match', 'once');
- end
- catch
- substring = '';
+ folderLevels = S.SubfolderLevel;
+
+ if isempty(strPattern) || isempty(folderLevels)
+ return
end
+
+ separator = '';
+ if isfield(S, 'Separator'); separator = S.Separator; end
+
+ combinedFolderName = nansen.config.dloc.DataLocationModel.combineFolderNamesFromPath( ...
+ pathStr, folderLevels, S.NumSubfolders, separator);
+
+ substring = nansen.config.dloc.DataLocationModel.applyExtractionPattern( ...
+ combinedFolderName, mode, strPattern);
end
end
-
+
methods (Access = protected) % Override superclass methods
-
+
function S = cleanStructOnSave(obj, S)
%cleanStructOnSave DataLocationModel specific changes when saving
for i = 1:numel(S.Data)
S.Data(i).Type = S.Data(i).Type.Name;
end
-
+
% Export local paths and restore original paths using LocalRootPathManager
[S.Data, ~] = obj.LocalRootPathManager.exportLocalRootPaths(S.Data, S.Preferences);
-
end
-
+
function S = modifyStructOnLoad(obj, S)
%modifyStructOnLoad DataLocationModel specific changes when loading
%
% Create type object instance.
+
for i = 1:numel(S.Data)
if ~isfield(S.Data(i), 'Type') || isempty(S.Data(i).Type)
S.Data(i).Type = nansen.config.dloc.DataLocationType('recorded');
@@ -793,18 +803,18 @@ function restore(obj, data)
S.Data(i).Type = nansen.config.dloc.DataLocationType(S.Data(i).Type);
end
end
-
+
if ~isfield(S.Preferences, 'SourceID')
S.Preferences.SourceID = utility.system.getComputerName(true);
end
-
+
S = obj.updateRootPathDataType(S); % Todo_ temp: remove before release
-
+
% Import local root paths using LocalRootPathManager
if isempty(obj.LocalRootPathManager)
obj.initializeLocalRootPathManager();
if isempty(obj.LocalRootPathManager)
- % If it is still empty,
+ % If it is still empty,
return
end
end
@@ -815,12 +825,12 @@ function restore(obj, data)
S.Data = obj.LocalRootPathManager.updateRootPathFromDiskName(S.Data);
end
end
-
+
methods (Access = private)
function onDataLocationRenamed(obj, oldName, newName)
-
+
obj.assignItemNames()
-
+
% Update value default data location if this was the one that
% was renamed..
if strcmp(obj.DefaultDataLocation, oldName)
@@ -828,11 +838,11 @@ function onDataLocationRenamed(obj, oldName, newName)
end
end
end
-
+
methods (Access = private) % Internal
-
+
function updateMetadataExtractorFunctionNames(obj)
-
+
% TODO: This should not be hardcoded here. Ideally users can
% also add their own variables.
variableNames = {'SubjectId', 'SessionId', 'ExperimentDate', 'ExperimentTime'};
@@ -849,7 +859,7 @@ function updateMetadataExtractorFunctionNames(obj)
end
obj.save()
end
-
+
function initializeLocalRootPathManager(obj)
import nansen.config.project.ProjectManager
localProjectFolder = string(ProjectManager.getProjectPath('current', 'local'));
@@ -858,16 +868,16 @@ function initializeLocalRootPathManager(obj)
end
end
end
-
+
methods %(Access = ?nansen.config.project.Project)
-
+
function onProjectRenamed(obj, oldName, newName)
% onProjectRenamed - Rename configs that depend on project name
-
+
% Note: Function names for extracting data identifiers
% (subjectId, sessionId, experimentData & experimentTime) depend on
% the project name
-
+
for i = 1:obj.NumDataLocations
for j = 1:numel(obj.Data(i).MetaDataDef)
if isfield(obj.Data(i).MetaDataDef(j), 'FunctionName')
@@ -884,10 +894,10 @@ function onProjectRenamed(obj, oldName, newName)
end
methods (Static)
-
+
function pathString = getDefaultFilePath()
%getFilePath Get filepath for loading/saving datalocation settings
-
+
error('NANSEN:DefaultDataLocationNotImplemented', ...
['Please specify a file path for a data location model. ' ...
'There is currently no default data location model.'])
@@ -919,11 +929,11 @@ function addTypeAsTableVariable(obj)
for i = 1:numel(obj.Data)
obj.Data(i).Type = 'recorded';
end
-
+
obj.Data = orderfields(obj.Data, ...
[fieldNamesOld(1:2); 'Type'; fieldNamesOld(3:end)]);
end
-
+
function addDiskNameToAllRootPaths(obj)
for i = 1:numel(obj.Data)
obj.Data(i).RootPath = obj.addDiskNameToRootPathStruct(obj.Data(i).RootPath);
@@ -931,6 +941,7 @@ function addDiskNameToAllRootPaths(obj)
end
function rootPathStruct = addDiskNameToRootPathStruct(obj, rootPathStruct)
+
for i = 1:numel(rootPathStruct)
rootPathStruct(i).DiskName = ...
obj.resolveDiskName(rootPathStruct(i).Value);
@@ -945,30 +956,31 @@ function addDiskTypeToAllRootPaths(obj)
end
end
end
-
+
methods (Static)
-
+
function S = updateRootPathDataType(S) % TEMP: Todo: remove
%updateRootPathDataType
-
+
% Todo: Should we make struct array instead, with key value
% fields and use universal unique ids????
-
+
% Update root data type from cell array to struct.
+
if numel(S.Data) > 0
if isa(S.Data(1).RootPath, 'cell')
for i = 1:numel(S.Data)
-
+
sNew = struct();
for j = 1:numel(S.Data(i).RootPath)
sNew(j).Key = nansen.util.getuuid();
sNew(j).Value = S.Data(i).RootPath{j};
sNew(j).Diskname = '';
end
-
+
S.Data(i).RootPath = sNew;
end
-
+
elseif isa(S.Data(1).RootPath, 'struct') && ~isfield(S.Data(1).RootPath, 'Key')
for i = 1:numel(S.Data)
rootKeys = fieldnames(S.Data(i).RootPath);
@@ -980,7 +992,7 @@ function addDiskTypeToAllRootPaths(obj)
end
elseif isa(S.Data(1).RootPath, 'struct') && isempty(S.Data(1).RootPath)
return
-
+
elseif isa(S.Data(1).RootPath, 'struct') && isfield(S.Data(1).RootPath, 'Key') && isa(S.Data(1).RootPath(1).Key, 'cell')
for i = 1:numel(S.Data)
S.Data(i).RootPath(1).Key = nansen.util.getuuid();
@@ -990,4 +1002,119 @@ function addDiskTypeToAllRootPaths(obj)
end
end
end
+
+ methods (Static) % Methods for inferring default metadata configuration
+
+ function subfolderIndex = getDefaultSubfolderLevelForVariable(variableName, subfolderStructure)
+ %getDefaultSubfolderLevelForVariable Infer the default subfolder level for a metadata variable
+ %
+ % subfolderIndex = getDefaultSubfolderLevelForVariable(variableName, subfolderStructure)
+ % returns the index into subfolderStructure whose Type matches the
+ % semantic role of variableName, or 0 if no match is found.
+ %
+ % This encodes the mapping between metadata variable names
+ % ('Subject ID', 'Session ID', 'Experiment Date') and subfolder
+ % types ('Subject', 'Session', 'Date') as defined by the
+ % SubfolderStructure of a data location.
+
+ arguments
+ variableName (1,:) char
+ subfolderStructure struct
+ end
+
+ switch variableName
+ case 'Subject ID'
+ subfolderIndex = find(strcmp({subfolderStructure.Type}, 'Subject'), 1);
+ case 'Session ID'
+ subfolderIndex = find(strcmp({subfolderStructure.Type}, 'Session'), 1);
+ case {'Date', 'Experiment Date'}
+ subfolderIndex = find(strcmp({subfolderStructure.Type}, 'Date'), 1);
+ otherwise
+ subfolderIndex = [];
+ end
+ if isempty(subfolderIndex)
+ subfolderIndex = 0;
+ end
+ end
+ end
+
+ methods (Static, Hidden) % Low-level extraction utilities
+
+ function combinedName = combineFolderNamesFromPath( ...
+ pathStr, folderLevels, numSubfolders, separator)
+ % combineFolderNamesFromPath Collect and join folder names at given levels
+ %
+ % Counts subfolder positions backward from the deepest level so
+ % that the indices in folderLevels are independent of the root
+ % path depth. Returns an empty string if folderLevels is empty.
+
+ if isempty(folderLevels)
+ combinedName = '';
+ return
+ end
+
+ folderPathParts = strsplit(pathStr, filesep);
+
+ folderNameParts = cell(1, numel(folderLevels));
+ for k = 1:numel(folderLevels)
+ reversedIdx = numSubfolders - folderLevels(k);
+ folderIdx = numel(folderPathParts) - reversedIdx;
+ if folderIdx >= 1 && folderIdx <= numel(folderPathParts)
+ folderNameParts{k} = folderPathParts{folderIdx};
+ else
+ folderNameParts{k} = '';
+ end
+ end
+
+ combinedName = strjoin(folderNameParts, separator);
+ end
+
+ function substring = applyExtractionPattern(text, mode, pattern)
+ %applyExtractionPattern Apply an ind or expr extraction rule to text
+ %
+ % mode 'ind' — evaluates pattern as a character index expression
+ % (e.g. '1:5', '3:end')
+ % mode 'expr' — treats pattern as a regexp and returns first match
+
+ arguments
+ text
+ mode
+ pattern
+ end
+
+ switch lower(mode)
+ case 'ind'
+
+ if strcmp(pattern, '1:end') % Special case
+ pattern = sprintf('1:%d', strlength(text));
+ end
+ try
+ indices = eval(['[' pattern ']']);
+ catch
+ error('NANSEN:DataLocationModel:InvalidIndexPattern', ...
+ 'Index pattern "%s" is not valid MATLAB index syntax.', pattern)
+ end
+ if any(indices > strlength(text))
+ error('NANSEN:DataLocationModel:IndexOutOfRange', ...
+ 'Index range [%s] exceeds the string length (%d characters).', ...
+ pattern, strlength(text))
+ end
+ substring = text(indices);
+ case 'expr'
+ try
+ result = regexp(text, pattern, 'match', 'once');
+ catch
+ error('NANSEN:DataLocationModel:InvalidRegexPattern', ...
+ 'Regular expression "%s" is not valid.', pattern)
+ end
+ if isempty(result)
+ substring = '';
+ else
+ substring = result;
+ end
+ otherwise
+ substring = '';
+ end
+ end
+ end
end
diff --git a/code/+nansen/+config/+dloc/@DataLocationModel/getBlankItem.m b/code/+nansen/+config/+dloc/@DataLocationModel/getBlankItem.m
index a565087a..2ddc859f 100644
--- a/code/+nansen/+config/+dloc/@DataLocationModel/getBlankItem.m
+++ b/code/+nansen/+config/+dloc/@DataLocationModel/getBlankItem.m
@@ -12,5 +12,4 @@
S.SubfolderStructure = DataLocationModel.getDefaultSubfolderStructure();
S.MetaDataDef = DataLocationModel.getDefaultMetadataStructure();
-
end
diff --git a/code/+nansen/+config/+dloc/@DataLocationModel/getDefaultItem.m b/code/+nansen/+config/+dloc/@DataLocationModel/getDefaultItem.m
index 922e18e3..2d0a2f7c 100644
--- a/code/+nansen/+config/+dloc/@DataLocationModel/getDefaultItem.m
+++ b/code/+nansen/+config/+dloc/@DataLocationModel/getDefaultItem.m
@@ -17,27 +17,26 @@
S(i) = nansen.config.dloc.DataLocationModel.getBlankItem();
S(i).Name = 'Rawdata';
S(i).Type = nansen.config.dloc.DataLocationType('recorded');
- %S(i).RootPath = {'', ''};
- %S(i).ExamplePath = '';
- %S(i).DataSubfolders = {};
-
- %S(i).SubfolderStructure = struct('Name', {}, 'Type', {}, 'Expression', {}, 'IgnoreList', {});
+ % S(i).RootPath = {'', ''};
+ % S(i).ExamplePath = '';
+ % S(i).DataSubfolders = {};
+
+ % S(i).SubfolderStructure = struct('Name', {}, 'Type', {}, 'Expression', {}, 'IgnoreList', {});
S(i).SubfolderStructure(1).Name = '';
S(i).SubfolderStructure(1).Type = '';
S(i).SubfolderStructure(1).Expression = '';
S(i).SubfolderStructure(1).IgnoreList = {};
-
+
i = i + 1;
S(i) = nansen.config.dloc.DataLocationModel.getBlankItem();
S(i).Name = 'Processed';
S(i).Type = nansen.config.dloc.DataLocationType('processed');
- %S(i).RootPath = {'', ''};
- %S(i).ExamplePath = '';
- %S(i).DataSubfolders = {};
+ % S(i).RootPath = {'', ''};
+ % S(i).ExamplePath = '';
+ % S(i).DataSubfolders = {};
S(i).SubfolderStructure(1).Type = 'Subject';
S(i).SubfolderStructure(2).Type = 'Session';
-
+
P.DefaultDataLocation = 'Processed';
-
end
diff --git a/code/+nansen/+config/+dloc/@DataLocationModel/listDataFolders.m b/code/+nansen/+config/+dloc/@DataLocationModel/listDataFolders.m
index c4ad8974..968fc290 100644
--- a/code/+nansen/+config/+dloc/@DataLocationModel/listDataFolders.m
+++ b/code/+nansen/+config/+dloc/@DataLocationModel/listDataFolders.m
@@ -18,7 +18,7 @@
end
dataFolders = struct; % Initialize output
-
+
for i = ind
rootPath = {obj.Data(i).RootPath.Value};
S = obj.Data(i).SubfolderStructure;
diff --git a/code/+nansen/+config/+dloc/@MetadataInitializationUI/MetadataInitializationUI.m b/code/+nansen/+config/+dloc/@MetadataInitializationUI/MetadataInitializationUI.m
index a8d5892f..cf546125 100644
--- a/code/+nansen/+config/+dloc/@MetadataInitializationUI/MetadataInitializationUI.m
+++ b/code/+nansen/+config/+dloc/@MetadataInitializationUI/MetadataInitializationUI.m
@@ -1,65 +1,70 @@
classdef MetadataInitializationUI < applify.apptable & nansen.config.mixin.HasDataLocationModel
-% Class interface for editing metadata specifications in a uifigure
+% MetadataInitializationUI - UI panel for configuring session metadata extraction rules
%
+% Provides a table-based interface where the user defines how each
+% session metadata variable (Subject ID, Session ID, Experiment Date,
+% Experiment Time) is extracted from a folder path string. Three
+% extraction modes are supported: index-based substring selection,
+% regular expression matching, and a custom MATLAB function.
%
+% The panel reads its initial state from a DataLocationModel and writes
+% back to it when the user applies changes. A toolbar dropdown allows
+% switching between data locations when multiple are configured.
+%
+% See also nansen.config.dloc.DataLocationModel
-% Note: The data in this ui will only depend on the first datalocation. It
-% might be an idea to let the user select which data location to use for
-% detecting session information, but for simplicity the first data location
-% is used.
% Todo: Simplify component creation.
% [ ] Get cell locations as array with one entry for each column of a row.
% [ ] Do the centering when getting the cell locations.
% [ ] Set fontsize/bg color and other properties in batch.
%
-% [ ]Update DL Model whenever new values are entered. - Why???
+% [ ] Update DL Model whenever new values are entered. - Why???
%
-% [ ] Fix error that will occur if several subfolders are
-% given the same subfolder type?
properties
IsDirty = false;
IsAdvancedView = false
end
-
+
properties (SetAccess = private) % Todo: make this public when support for changing it is added.
DataLocationIndex = 1; %Todo: Select which dloc to use...
end
-
+
properties (Access = protected)
StringFormat = cell(1, 4); % Store stringformat for each session metadata item. Relevant for date and time.
FunctionName = cell(1, 4)
+ Separator = cell(1, 4) % Store folder-level separator for each session metadata item.
% Todo: This should be incorporated better, saving directly to the model.
end
-
+
properties (Access = private) % Toolbar Components
SelectDatalocationDropDownLabel
SelectDataLocationDropDown
AdvancedOptionsButton
end
-
+
methods % Structors
function obj = MetadataInitializationUI(dataLocationModel, varargin)
%FolderOrganizationUI Construct a FolderOrganizationUI instance
-
+
obj@nansen.config.mixin.HasDataLocationModel(dataLocationModel)
-
+
% Todo: Make it possible to select which datalocation to use..
varargin = [varargin, {'Data', dataLocationModel.Data(1).MetaDataDef}];
obj@applify.apptable(varargin{:})
-
+
obj.onModelSet()
-
+
% Reset IsDirty flag because it will be triggered when model is
% set.
obj.IsDirty = false;
end
end
-
+
methods (Access = protected) % Methods for creation
-
+
function assignDefaultTablePropertyValues(obj)
obj.ColumnNames = {'Variable name', 'Select foldername', ...
@@ -69,43 +74,54 @@ function assignDefaultTablePropertyValues(obj)
obj.RowSpacing = 20;
obj.ColumnSpacing = 25;
end
-
- function hRow = createTableRowComponents(obj, rowData, rowNum)
-
+
+ function hRow = createTableRowComponents(obj, rowData, rowNumber)
+
hRow = struct();
-
- %rootPath = mfilename('fullpath') ;
- %imgPath = fullfile(rootPath, '_graphics');
-
+
+ % rootPath = mfilename('fullpath') ;
+ % imgPath = fullfile(rootPath, '_graphics');
+
% % Create VariableName label
i = 1;
- [xi, y, wi, h] = obj.getCellPosition(rowNum, i);
-
+ [xi, y, wi, h] = obj.getCellPosition(rowNumber, i);
+
hRow.VariableName = uilabel(obj.TablePanel);
hRow.VariableName.Position = [xi y wi h];
hRow.VariableName.FontName = obj.FontName;
obj.centerComponent(hRow.VariableName, y)
-
+
hRow.VariableName.Text = rowData.VariableName;
-
+
% % Create Filename Expression edit field
i = 2;
- [xi, y, wi, h] = obj.getCellPosition(rowNum, i);
+ [xi, y, wi, h] = obj.getCellPosition(rowNumber, i);
hRow.FolderNameSelector = uidropdown(obj.TablePanel);
hRow.FolderNameSelector.BackgroundColor = [1 1 1];
hRow.FolderNameSelector.Position = [xi y wi h];
hRow.FolderNameSelector.FontName = obj.FontName;
hRow.FolderNameSelector.ValueChangedFcn = @obj.onFolderNameSelectionChanged;
+ hRow.FolderNameSelector.UserData = struct('FolderItems', {{}});
obj.centerComponent(hRow.FolderNameSelector, y)
-
- % Todo: Get folders from DataLocation.
hRow.FolderNameSelector.Items = {'Select foldername...'};
hRow.FolderNameSelector.Value = 'Select foldername...';
-
+
+ % Button shown in place of the dropdown when multiple folder
+ % levels are selected (initially hidden).
+ hRow.FolderMultiSelector = uibutton(obj.TablePanel);
+ hRow.FolderMultiSelector.Position = [xi y wi h];
+ hRow.FolderMultiSelector.FontName = obj.FontName;
+ hRow.FolderMultiSelector.HorizontalAlignment = 'left';
+ hRow.FolderMultiSelector.Text = 'Select folder(s)...';
+ hRow.FolderMultiSelector.ButtonPushedFcn = @obj.onFolderSelectorButtonPushed;
+ hRow.FolderMultiSelector.UserData = struct('FolderItems', {{}}, 'SelectedIndices', []);
+ hRow.FolderMultiSelector.Visible = 'off';
+ obj.centerComponent(hRow.FolderMultiSelector, y)
+
% % Create Togglebutton group for selecting string detection mode
i = 3;
- [xi, y, wi, h] = obj.getCellPosition(rowNum, i);
+ [xi, y, wi, h] = obj.getCellPosition(rowNumber, i);
% Insert dialog button
hRow.SelectSubstringButton = uibutton(obj.TablePanel);
@@ -113,55 +129,55 @@ function assignDefaultTablePropertyValues(obj)
hRow.SelectSubstringButton.Text = 'Select Substring...';
hRow.SelectSubstringButton.ButtonPushedFcn = @obj.onSelectSubstringButtonPushed;
obj.centerComponent(hRow.SelectSubstringButton, y)
-
+
% Create button group
- hRow.ButtonGroupStrfindMode = uibuttongroup(obj.TablePanel);
- hRow.ButtonGroupStrfindMode.BorderType = 'none';
- hRow.ButtonGroupStrfindMode.BackgroundColor = [1 1 1];
- hRow.ButtonGroupStrfindMode.Position = [xi y wi h];
- hRow.ButtonGroupStrfindMode.FontName = obj.FontName;
- hRow.ButtonGroupStrfindMode.ButtonDownFcn = ...
- @obj.onButtonGroupStrfindModeButtonDown;
- hRow.ButtonGroupStrfindMode.SelectionChangedFcn = ...
- @obj.onButtonGroupStrfindModeButtonDown;
-
- obj.centerComponent(hRow.ButtonGroupStrfindMode, y)
-
+ hRow.StringExtractModeButtonGroup = uibuttongroup(obj.TablePanel);
+ hRow.StringExtractModeButtonGroup.BorderType = 'none';
+ hRow.StringExtractModeButtonGroup.BackgroundColor = [1 1 1];
+ hRow.StringExtractModeButtonGroup.Position = [xi y wi h];
+ hRow.StringExtractModeButtonGroup.FontName = obj.FontName;
+ hRow.StringExtractModeButtonGroup.ButtonDownFcn = ...
+ @obj.onStringExtractModeChanged;
+ hRow.StringExtractModeButtonGroup.SelectionChangedFcn = ...
+ @obj.onStringExtractModeChanged;
+
+ obj.centerComponent(hRow.StringExtractModeButtonGroup, y)
+
% Create ModeButton1
- ModeButton1 = uitogglebutton(hRow.ButtonGroupStrfindMode);
+ ModeButton1 = uitogglebutton(hRow.StringExtractModeButtonGroup);
ModeButton1.Text = 'ind';
ModeButton1.Position = [1 1 41 22];
ModeButton1.Value = true;
% Create ModeButton2
- ModeButton2 = uitogglebutton(hRow.ButtonGroupStrfindMode);
+ ModeButton2 = uitogglebutton(hRow.StringExtractModeButtonGroup);
ModeButton2.Text = 'expr';
ModeButton2.Position = [41 1 41 22];
% Create ModeButton3
- ModeButton3 = uitogglebutton(hRow.ButtonGroupStrfindMode);
+ ModeButton3 = uitogglebutton(hRow.StringExtractModeButtonGroup);
ModeButton3.Text = 'func';
ModeButton3.Position = [81 1 41 22];
-
+
% % Create Editbox for string expression input
i = 4;
- [xi, y, wi, h] = obj.getCellPosition(rowNum, i);
-
- hRow.StrfindInputEditbox = uieditfield(obj.TablePanel, 'text');
- hRow.StrfindInputEditbox.Position = [xi y wi h];
- hRow.StrfindInputEditbox.FontName = obj.FontName;
- hRow.StrfindInputEditbox.ValueChangedFcn = @obj.onStringInputValueChanged;
-
- obj.centerComponent(hRow.StrfindInputEditbox, y)
- hRow.StrfindInputEditbox.Enable = 'on';
-
+ [xi, y, wi, h] = obj.getCellPosition(rowNumber, i);
+
+ hRow.StringExtractInputField = uieditfield(obj.TablePanel, 'text');
+ hRow.StringExtractInputField.Position = [xi y wi h];
+ hRow.StringExtractInputField.FontName = obj.FontName;
+ hRow.StringExtractInputField.ValueChangedFcn = @obj.onStringInputValueChanged;
+
+ obj.centerComponent(hRow.StringExtractInputField, y)
+ hRow.StringExtractInputField.Enable = 'on';
+
if ~isempty(rowData.StringDetectInput)
- hRow.StrfindInputEditbox.Value = rowData.StringDetectInput;
+ hRow.StringExtractInputField.Value = rowData.StringDetectInput;
end
% % Advanced (function) Create buttons for edit/running function
import uim.utility.layout.subdividePosition
-
+
[xii, wii] = subdividePosition(xi, wi, [0.5,0.5], 5);
hRow.EditFunctionButton = uibutton(obj.TablePanel);
hRow.EditFunctionButton.Text = 'Edit';
@@ -181,38 +197,63 @@ function assignDefaultTablePropertyValues(obj)
% % Create Editbox to show detected string.
i = 5;
- [xi, y, wi, h] = obj.getCellPosition(rowNum, i);
+ [xi, y, wi, h] = obj.getCellPosition(rowNumber, i);
- hRow.StrfindResultEditbox = uieditfield(obj.TablePanel, 'text');
- hRow.StrfindResultEditbox.Position = [xi y wi h];
- hRow.StrfindResultEditbox.Enable = 'off';
- hRow.StrfindResultEditbox.FontName = obj.FontName;
- obj.centerComponent(hRow.StrfindResultEditbox, y)
+ hRow.StringExtractResultField = uieditfield(obj.TablePanel, 'text');
+ hRow.StringExtractResultField.Position = [xi y wi h];
+ hRow.StringExtractResultField.Enable = 'off';
+ hRow.StringExtractResultField.FontName = obj.FontName;
+ obj.centerComponent(hRow.StringExtractResultField, y)
end
-
+
function createToolbarComponents(obj, hPanel)
%createToolbarComponents Create "toolbar" components above table.
if nargin < 2; hPanel = obj.Parent.Parent; end
-
+
obj.createAdvancedOptionsButton(hPanel)
obj.createDataLocationSelector(hPanel)
end
-
+
function toolbarComponents = getToolbarComponents(obj)
toolbarComponents = obj.AdvancedOptionsButton;
end
end
-
- methods (Access = private) %Callbacks for userinteraction with controls
-
+
+ methods (Access = private) % Callbacks for userinteraction with controls
+
function onFolderNameSelectionChanged(obj, src, ~)
- % Add value to tooltip of control
-
+ % Callback for the folder-level dropdown
+
rowNumber = obj.getComponentRowNumber(src);
- idx = obj.getSubfolderLevel(rowNumber);
-
- obj.Data(rowNumber).SubfolderLevel = idx;
-
+
+ if strcmp(src.Value, 'Select multiple folders...')
+ % Open multi-select dialog
+ hFig = ancestor(src, 'figure');
+ folderItems = src.UserData.FolderItems;
+ currentSeparator = obj.Separator{rowNumber};
+ if isempty(currentSeparator); currentSeparator = ''; end
+
+ [selectedIndices, separator] = obj.showFolderSelectorDialog( ...
+ folderItems, [], currentSeparator, hFig.Position);
+
+ if numel(selectedIndices) > 1
+ obj.exitFuncModeIfActive(rowNumber);
+ obj.switchToMultiSelectMode(rowNumber, selectedIndices, separator);
+ elseif isscalar(selectedIndices)
+ % User picked exactly one — stay in dropdown mode
+ obj.exitFuncModeIfActive(rowNumber);
+ src.Value = folderItems{selectedIndices};
+ else
+ % User cancelled — reset dropdown, leave mode unchanged
+ src.Value = src.Items{1};
+ return
+ end
+ else
+ obj.exitFuncModeIfActive(rowNumber);
+ end
+
+ obj.Data(rowNumber).SubfolderLevel = obj.getSubfolderLevel(rowNumber);
+
try
obj.updateStringResult(rowNumber)
catch ME
@@ -223,50 +264,94 @@ function onFolderNameSelectionChanged(obj, src, ~)
uialert(hFig, ME.message, 'Update Failed')
end
- obj.updateStringResult(rowNumber)
+ src.Tooltip = src.Value;
+ obj.IsDirty = true;
+ end
+
+ function onFolderSelectorButtonPushed(obj, src, ~)
+ % Callback for the multi-select button (shown when >1 levels selected)
- %obj.onStringInputValueChanged(hComp)
+ rowNumber = obj.getComponentRowNumber(src);
+ folderItems = src.UserData.FolderItems;
+ currentIndices = src.UserData.SelectedIndices;
+ currentSeparator = obj.Separator{rowNumber};
+ if isempty(currentSeparator); currentSeparator = ''; end
+
+ hFig = ancestor(src, 'figure');
+ [selectedIndices, separator] = obj.showFolderSelectorDialog( ...
+ folderItems, currentIndices, currentSeparator, hFig.Position);
+
+ if isequal(selectedIndices, currentIndices) && strcmp(separator, currentSeparator)
+ return % No change — leave mode unchanged
+ end
+
+ obj.exitFuncModeIfActive(rowNumber);
+
+ if isscalar(selectedIndices)
+ % Reduced to a single folder — revert to dropdown
+ obj.switchToSingleSelectMode(rowNumber, selectedIndices);
+ elseif numel(selectedIndices) > 1
+ src.UserData.SelectedIndices = selectedIndices;
+ obj.Separator{rowNumber} = separator;
+ obj.updateFolderSelectorButtonText(src, folderItems, selectedIndices);
+ else
+ % User cleared the selection — revert to dropdown, no selection
+ obj.switchToSingleSelectMode(rowNumber, []);
+ end
+
+ obj.Data(rowNumber).SubfolderLevel = obj.getSubfolderLevel(rowNumber);
+
+ try
+ obj.updateStringResult(rowNumber)
+ catch ME
+ if strcmp(ME.identifier, 'MATLAB:badsubscript')
+ ME = obj.getModifiedBadSubscriptException();
+ end
+ uialert(hFig, ME.message, 'Update Failed')
+ end
- src.Tooltip = src.Value;
obj.IsDirty = true;
end
- function onSelectSubstringButtonPushed(obj, src, evt)
+ function onSelectSubstringButtonPushed(obj, src, ~)
% Open a dialog window for selecting letter positions.
-
+
% Get foldername for the row which user pushed button from
rowNumber = obj.getComponentRowNumber(src);
hRow = obj.RowControls(rowNumber);
- folderName = hRow.FolderNameSelector.Value;
-
+
+ % Build the combined folder string from the selected levels and
+ % separator — this is the string the pattern is applied to.
+ folderName = obj.getCombinedFolderName(rowNumber);
+
% Create a dialog where the user can select a substring from
% the foldername
hFig = ancestor(src, 'figure');
- IND = uim.dialog.createStringSelectorDialog(folderName, hFig.Position);
+ selectedIndices = uim.dialog.createStringSelectorDialog(folderName, hFig.Position);
% Return if user canceled...
- if isempty(IND)
+ if isempty(selectedIndices)
pause(0.1)
figure(hFig) % Bring uifigure back to focus
return
% ...Or update data and controls
else
- hRow.StrfindInputEditbox.Value = obj.simplifyInd(IND);
+ hRow.StringExtractInputField.Value = obj.simplifyIndices(selectedIndices);
% If the variable is date or time, try to convert to
% datetime value:
if obj.isDateTimeVariable(hRow.VariableName.Text)
-
+
shortName = strrep(hRow.VariableName.Text, 'Experiment', '');
-
- substring = obj.getFolderSubString(rowNumber);
+
+ substring = obj.getFolderSubstring(rowNumber);
[dtInFormat, dtOutFormat] = obj.uiGetDateTimeFormat(hRow.VariableName.Text, substring);
-
+
if ~isempty(dtInFormat)
try
datetimeValue = datetime(substring, 'InputFormat', dtInFormat);
datetimeValue.Format = dtOutFormat;
- hRow.StrfindResultEditbox.Value = char(datetimeValue);
+ hRow.StringExtractResultField.Value = char(datetimeValue);
obj.StringFormat{rowNumber} = dtInFormat;
catch ME
uialert(hFig, ME.message, sprintf('%s Format Error', shortName))
@@ -279,42 +364,40 @@ function onSelectSubstringButtonPushed(obj, src, evt)
obj.updateStringResult(rowNumber)
end
end
-
+
obj.IsDirty = true;
-
+
figure(hFig) % Bring uifigure back into focus
end
- function onStringInputValueChanged(obj, src, event)
+ function onStringInputValueChanged(obj, src, ~)
%onStringInputValueChanged Updates result editfield when the string
% input/selection indices are modified.
-
- substring = '';
-
+
thisDataLocation = obj.DataLocationModel.Data(obj.DataLocationIndex);
- M = thisDataLocation.MetaDataDef;
-
+ metadataDefinitions = thisDataLocation.MetaDataDef;
+
rowNumber = obj.getComponentRowNumber(src);
hRow = obj.RowControls(rowNumber);
- identifierName = obj.getIdNameForRow(rowNumber);
+ identifierName = obj.getIdentifierNameForRow(rowNumber);
try
- substring = obj.getFolderSubString(rowNumber);
+ substring = obj.getFolderSubstring(rowNumber);
catch ME
hFig = ancestor(src, 'figure');
substring = 'N/A';
errorMessage = sprintf('Failed to extract "%s" from folder path. Caused by:\n\n%s', identifierName, ME.message);
uialert(hFig, errorMessage, 'String extraction failed')
end
-
+
% Convert date/time value if date/time format is available
- if obj.isDateTimeVariable(M(rowNumber).VariableName)
+ if obj.isDateTimeVariable(metadataDefinitions(rowNumber).VariableName)
if isa(substring, 'datetime')
substring = char(substring);
else
examplePath = thisDataLocation.ExamplePath;
try
- switch M(rowNumber).VariableName
+ switch metadataDefinitions(rowNumber).VariableName
case 'Experiment Time'
value = obj.DataLocationModel.getTime(examplePath, obj.DataLocationIndex);
case 'Experiment Date'
@@ -327,41 +410,38 @@ function onStringInputValueChanged(obj, src, event)
end
end
- hRow.StrfindResultEditbox.Value = substring;
- hRow.StrfindResultEditbox.Tooltip = substring;
-
+ hRow.StringExtractResultField.Value = substring;
+ hRow.StringExtractResultField.Tooltip = substring;
+
obj.IsDirty = true;
end
-
+
function onRunFunctionButtonClicked(obj, src, evt)
rowNumber = obj.getComponentRowNumber(src);
+ functionExists = false;
if ~isempty(obj.FunctionName{rowNumber})
try
feval(obj.FunctionName{rowNumber}, '', '')
+ functionExists = true;
catch ME
- existFunction = ~strcmp(ME.identifier, 'MATLAB:UndefinedFunction');
+ functionExists = ~strcmp(ME.identifier, 'MATLAB:UndefinedFunction');
end
- else
- existFunction = false;
end
- if ~existFunction
- hFig = ancestor(src, 'figure');
- message = sprintf( ['The function does not exist yet. ', ...
- 'Please press "Edit" to initialize the function from a template.']);
- uialert(hFig, message, 'Function missing...','Icon', 'info')
+ if ~functionExists
+ obj.alertFunctionNotFound(rowNumber)
return
end
-
+
obj.onStringInputValueChanged(src, evt)
end
- function onEditFunctionButtonClicked(obj, src, evt)
-
+ function onEditFunctionButtonClicked(obj, src, ~)
+
rowNumber = obj.getComponentRowNumber(src);
-
- identifierName = obj.getIdNameForRow(rowNumber);
+
+ identifierName = obj.getIdentifierNameForRow(rowNumber);
functionName = createFunctionName(identifierName); % local function
pm = nansen.ProjectManager();
@@ -373,7 +453,7 @@ function onEditFunctionButtonClicked(obj, src, evt)
if ~isfile(functionFilePath)
nansen.config.dloc.createFunctionFromTemplate(dataLocations, identifierName, functionName)
-
+
hFig = ancestor(src, 'figure');
message = sprintf( ['The function "%s" will be opened in MATLAB''s editor. ', ...
'Please update the function so that it extracts the correct value ', ...
@@ -382,58 +462,74 @@ function onEditFunctionButtonClicked(obj, src, evt)
edit(functionFilePath)
else
% Todo :
- %nansen.config.dloc.updateFunctionTemplate(functionFilePath, dataLocations);
+ % nansen.config.dloc.updateFunctionTemplate(functionFilePath, dataLocations);
edit(functionFilePath)
end
-
- fullFuntionName = utility.path.abspath2funcname(functionFilePath);
- obj.FunctionName{rowNumber} = fullFuntionName;
+
+ fullFunctionName = utility.path.abspath2funcname(functionFilePath);
+ obj.FunctionName{rowNumber} = fullFunctionName;
obj.IsDirty = true;
end
- function onButtonGroupStrfindModeButtonDown(obj, src, evt)
- % onButtonGroupStrfindModeButtonDown - Selection changed callback
-
+ function onStringExtractModeChanged(obj, src, ~)
+ % onStringExtractModeChanged Callback when string extract mode selection changes
+
rowNumber = obj.getComponentRowNumber(src);
obj.setFunctionButtonVisibility(rowNumber)
+ inputField = obj.RowControls(rowNumber).StringExtractInputField;
+ obj.onStringInputValueChanged(inputField)
end
- function onDataLocationSelectionChanged(obj, src, evt)
+ function onDataLocationSelectionChanged(obj, ~, evt)
% onDataLocationSelectionChanged - Dropdown value changed callback
- newInd = obj.DataLocationModel.getItemIndex(evt.Value);
-
+ newIndex = obj.DataLocationModel.getItemIndex(evt.Value);
+
% Update datalocationmodel with data from ui
obj.updateDataLocationModel()
% Change current data location
- obj.DataLocationIndex = newInd;
+ obj.DataLocationIndex = newIndex;
end
end
methods % Methods for updating the Result column
- function substring = getFolderSubString(obj, rowNumber)
- %getFolderSubString Get folder substring based on user selections
- mode = obj.getStrSearchMode(rowNumber);
- strPattern = obj.getStrSearchPattern(rowNumber, mode);
- folderName = obj.RowControls(rowNumber).FolderNameSelector.Value;
-
- switch lower(mode)
- case 'ind'
- substring = eval( ['folderName([' strPattern '])'] );
-
- case 'expr'
- substring = regexp(folderName, strPattern, 'match', 'once');
-
- case 'func'
- substring = obj.getSubstringFromRowFunction(rowNumber);
+ function substring = getFolderSubstring(obj, rowNumber)
+ %getFolderSubstring Get folder substring based on current UI selections
+ %
+ % Builds an S struct from the current UI state and delegates the
+ % full extraction to DataLocationModel.getSubstringFromFolder.
+
+ dataLocationIndex = obj.DataLocationIndex;
+ thisDataLocation = obj.DataLocationModel.Data(dataLocationIndex);
+
+ S = struct();
+ S.StringDetectMode = obj.getStringExtractMode(rowNumber);
+ S.StringDetectInput = obj.getStringExtractPattern(rowNumber);
+ S.SubfolderLevel = obj.getSubfolderLevel(rowNumber);
+ S.Separator = obj.Separator{rowNumber};
+ S.NumSubfolders = numel(thisDataLocation.SubfolderStructure);
+ S.FunctionName = obj.FunctionName{rowNumber};
+
+ dataLocationName = thisDataLocation.Name;
+ substring = '';
+ try
+ substring = obj.DataLocationModel.getSubstringFromFolder( ...
+ thisDataLocation.ExamplePath, S, dataLocationName);
+ catch ME
+ switch ME.identifier
+ case 'NANSEN:DataLocationModel:FunctionNotFound'
+ obj.alertFunctionNotFound(rowNumber)
+ otherwise
+ rethrow(ME)
+ end
end
end
end
-
+
methods % Methods for updating
-
+
function set.DataLocationIndex(obj, newIndex)
obj.DataLocationIndex = newIndex;
obj.onModelSet()
@@ -442,182 +538,169 @@ function onDataLocationSelectionChanged(obj, src, evt)
function set.IsDirty(obj, newValue)
obj.IsDirty = newValue;
end
-
- function setActive(obj)
+
+ function setActive(obj) %#ok
%setActive Execute actions needed for ui activation
% Use if UI is part of an app with tabs, and the tab is selected
end
-
+
function setInactive(obj)
%setInactive Execute actions needed for ui inactivation
% Use if UI is part of an app with tabs, and the tab is unselected
obj.updateDataLocationModel()
end
-
+
function updateDataLocationModel(obj)
%updateDataLocationModel Update DLModel with changes from UI
S = obj.getMetaDataDefinitionStruct();
- dataLocationIdx = obj.DataLocationIndex;
- obj.DataLocationModel.updateMetaDataDefinitions(S, dataLocationIdx)
+ dataLocationIndex = obj.DataLocationIndex;
+ obj.DataLocationModel.updateMetaDataDefinitions(S, dataLocationIndex)
end
-
+
function S = getMetaDataDefinitionStruct(obj)
%getMetaDataDefinitionStruct Get struct of values from UI controls
-
+
S = obj.DataLocationModel.getDefaultMetadataStructure();
-
+
% Retrieve values from controls and add to struct
for i = 1:obj.NumRows
- S(i).StringDetectMode = obj.getStrSearchMode(i);
- S(i).StringDetectInput = obj.getStrSearchPattern(i);
+ S(i).StringDetectMode = obj.getStringExtractMode(i);
+ S(i).StringDetectInput = obj.getStringExtractPattern(i);
S(i).SubfolderLevel = obj.getSubfolderLevel(i);
S(i).StringFormat = obj.StringFormat{i};
+ S(i).Separator = obj.Separator{i};
S(i).FunctionName = obj.FunctionName{i};
-
- if isnan(S(i).SubfolderLevel)
- % Revert to the original value if current value is nan.
- % Current value might be nan if there are currently no
- % available folders in the dropdown selector.
- S(i).SubfolderLevel = obj.Data(i).SubfolderLevel;
- end
end
end
-
+
function onModelSet(obj)
%onModelSet Callback for when DatalocationModel is set/reset
%
% % Update control values based on the DataLocationModel
-
- dlIdx = obj.DataLocationIndex;
- thisDataLocation = obj.DataLocationModel.Data(dlIdx);
-
+
+ dataLocationIndex = obj.DataLocationIndex;
+ thisDataLocation = obj.DataLocationModel.Data(dataLocationIndex);
+
% Update Items of subfolder dropdown
obj.setFolderSelectionItems()
-
+
% Update values of subfolder dropdown based on the metadata
% definitions
- M = thisDataLocation.MetaDataDef;
+ metadataDefinitions = thisDataLocation.MetaDataDef;
- % Update internal values from M
+ % Update internal values from metadataDefinitions
for i = 1:obj.NumRows
% Set stringformat from datalocation model.
- obj.StringFormat{i} = thisDataLocation.MetaDataDef(i).StringFormat;
+ obj.StringFormat{i} = metadataDefinitions(i).StringFormat;
+ if isfield(metadataDefinitions(i), 'Separator')
+ obj.Separator{i} = metadataDefinitions(i).Separator;
+ else
+ obj.Separator{i} = '';
+ end
try
- obj.FunctionName{i} = thisDataLocation.MetaDataDef(i).FunctionName;
+ obj.FunctionName{i} = metadataDefinitions(i).FunctionName;
catch
obj.FunctionName{i} = '';
end
end
- obj.updateFolderSelectionValue(M)
-
+ obj.updateFolderSelectionValue(metadataDefinitions)
+
% Update results
for i = 1:obj.NumRows
% Update detection mode
- obj.setStringSearchMode(i, M(i).StringDetectMode)
+ obj.setStringExtractMode(i, metadataDefinitions(i).StringDetectMode)
obj.setFunctionButtonVisibility(i)
- hComp = obj.RowControls(i).StrfindInputEditbox;
+ inputField = obj.RowControls(i).StringExtractInputField;
% Update value in string detection input
- hComp.Value = M(i).StringDetectInput;
- obj.onStringInputValueChanged(hComp)
+ inputField.Value = metadataDefinitions(i).StringDetectInput;
+ obj.refreshStringResult(i)
end
end
function setFolderSelectionItems(obj)
- %setFolderSelectionItems Add model's folder names to each dropdown
-
- % TODO: Fix error that will occur if several subfolders are
- % given the same subfolder type?
-
- dlIdx = obj.DataLocationIndex;
- thisDataLocation = obj.DataLocationModel.Data(dlIdx);
-
- % Get all the folder selector controls
- h = [obj.RowControls.FolderNameSelector];
-
- % Get the folder choice examples from the data location model
+ %setFolderSelectionItems Populate the folder-level dropdown items for each row
+
+ dataLocationIndex = obj.DataLocationIndex;
+ thisDataLocation = obj.DataLocationModel.Data(dataLocationIndex);
+
subFolderStructure = thisDataLocation.SubfolderStructure;
- folderChoices = ['Select foldername...', {subFolderStructure.Name}];
+ folderItems = {subFolderStructure.Name};
+ folderItems(cellfun(@isempty, folderItems)) = {'Foldername not found'};
- folderChoices(cellfun(@isempty, folderChoices)) = deal({'Foldername not found'});
- set(h, 'Items', folderChoices)
+ for i = 1:obj.NumRows
+ hRow = obj.RowControls(i);
+
+ % Store the clean folder list in both controls for index lookups.
+ hRow.FolderNameSelector.UserData.FolderItems = folderItems;
+ hRow.FolderMultiSelector.UserData.FolderItems = folderItems;
+
+ % Build dropdown items — Session ID gets the multi-select option.
+ dropdownItems = ['Select foldername...', folderItems];
+ if strcmp(hRow.VariableName.Text, 'Session ID')
+ dropdownItems = [dropdownItems, {'Select multiple folders...'}]; %#ok
+ end
+ hRow.FolderNameSelector.Items = dropdownItems;
+ end
end
- function updateFolderSelectionValue(obj, M)
- %updateFolderSelectionValue Set the dropdown value based on the model
- % Get all the folder selector controls
- h = [obj.RowControls.FolderNameSelector];
+ function updateFolderSelectionValue(obj, metadataDefinitions)
+ %updateFolderSelectionValue Restore the folder selection controls from the model
- dlIdx = obj.DataLocationIndex;
- thisDataLocation = obj.DataLocationModel.Data(dlIdx);
+ dataLocationIndex = obj.DataLocationIndex;
+ thisDataLocation = obj.DataLocationModel.Data(dataLocationIndex);
subFolderStructure = thisDataLocation.SubfolderStructure;
- for i = 1:numel(h)
- % Todo: Subfolder level needs to be reset if the number of
- % subfolders is changed in the model. See newt warning
- % below
- itemIdx = M(i).SubfolderLevel;
-
- % If there is no selection, try to infer from the data
- % organization.
- if isempty(itemIdx)
- itemIdx = obj.initFolderSelectionItemIndex(i, subFolderStructure);
+ for i = 1:obj.NumRows
+ folderItems = obj.RowControls(i).FolderNameSelector.UserData.FolderItems;
+
+ selectedItemIndex = metadataDefinitions(i).SubfolderLevel;
+
+ % If there is no selection, try to infer from the data organization.
+ if isempty(selectedItemIndex)
+ variableName = metadataDefinitions(i).VariableName;
+ selectedItemIndex = nansen.config.dloc.DataLocationModel.getDefaultSubfolderLevelForVariable( ...
+ variableName, subFolderStructure);
end
-
- if isempty(itemIdx)
- itemIdx = 0;
- elseif ~isscalar(itemIdx)
- itemIdx = itemIdx(1);
+
+ % Clamp to valid range (0 means no selection).
+ if isscalar(selectedItemIndex) && selectedItemIndex == 0
+ selectedItemIndex = [];
+ else
+ selectedItemIndex = selectedItemIndex(selectedItemIndex >= 1 & selectedItemIndex <= numel(folderItems));
end
-
- if itemIdx > numel(h(i).Items) - 1
- warning('Model is inconsistent. Working on fix')
+
+ if numel(selectedItemIndex) > 1
+ separator = '';
+ if isfield(metadataDefinitions(i), 'Separator'); separator = metadataDefinitions(i).Separator; end
+ obj.switchToMultiSelectMode(i, selectedItemIndex, separator);
else
- set(h(i), 'Value', h(i).Items{itemIdx+1})
+ obj.switchToSingleSelectMode(i, selectedItemIndex);
end
end
end
- function itemIdx = initFolderSelectionItemIndex(obj, rowNumber, subFolderStructure)
- %initFolderSelectionItemIndex Guess which index should be selected
- %
- % For each subfolder level in the folder organization, there is a
- % type. If the type matches with the current row, use the index
- % of that subfolder level as the initial choice.
-
- itemIdx = 0;
- switch obj.RowControls(rowNumber).VariableName.Text
- case 'Subject ID'
- isMatched = strcmp({subFolderStructure.Type}, 'Subject');
- if any(isMatched)
- itemIdx = find(isMatched);
- end
- case 'Session ID'
- isMatched = strcmp({subFolderStructure.Type}, 'Session');
- if any(isMatched)
- itemIdx = find(isMatched);
- end
- case {'Date', 'Experiment Date'}
- isMatched = strcmp({subFolderStructure.Type}, 'Date');
- if any(isMatched)
- itemIdx = find(isMatched);
- end
- case {'Time', 'Experiment Time'}
- itemIdx = 0;
- otherwise
- itemIdx = 0;
+ function refreshStringResult(obj, rowNumber)
+ %refreshStringResult Update result field for passive/programmatic updates.
+ % Extraction errors are shown inline in the result field — no alert.
+ hRow = obj.RowControls(rowNumber);
+ try
+ obj.updateStringResult(rowNumber)
+ catch ME
+ hRow.StringExtractResultField.Value = sprintf('error: %s', ME.message);
+ hRow.StringExtractResultField.Tooltip = ME.message;
end
end
-
+
function updateStringResult(obj, rowNumber)
-
+
hRow = obj.RowControls(rowNumber);
% Update values in editboxes
- substring = obj.getFolderSubString(rowNumber);
- hRow.StrfindResultEditbox.Value = char( substring );
- hRow.StrfindResultEditbox.Tooltip = char( substring );
+ substring = obj.getFolderSubstring(rowNumber);
+ hRow.StringExtractResultField.Value = char( substring );
+ hRow.StringExtractResultField.Tooltip = char( substring );
if ~isempty( obj.StringFormat{rowNumber} )
dtInFormat = obj.StringFormat{rowNumber};
@@ -627,8 +710,8 @@ function updateStringResult(obj, rowNumber)
datetimeValue.Format = dtOutFormat;
substring = char(datetimeValue);
- hRow.StrfindResultEditbox.Value = substring;
- hRow.StrfindResultEditbox.Tooltip = substring;
+ hRow.StringExtractResultField.Value = substring;
+ hRow.StringExtractResultField.Tooltip = substring;
end
end
@@ -644,118 +727,86 @@ function updateDataLocationSelector(obj)
end
end
end
-
+
methods
-
+
function markClean(obj)
obj.IsDirty = false;
end
-
- function mode = getStrSearchMode(obj, rowNumber)
-
- hBtnGroup = obj.RowControls(rowNumber).ButtonGroupStrfindMode;
- h = hBtnGroup.SelectedObject;
+
+ function mode = getStringExtractMode(obj, rowNumber)
+
+ buttonGroup = obj.RowControls(rowNumber).StringExtractModeButtonGroup;
+ h = buttonGroup.SelectedObject;
mode = h.Text;
end
- function setStringSearchMode(obj, rowNumber, value)
- hBtnGroup = obj.RowControls(rowNumber).ButtonGroupStrfindMode;
- hButtons = hBtnGroup.Children;
+ function setStringExtractMode(obj, rowNumber, value)
+ buttonGroup = obj.RowControls(rowNumber).StringExtractModeButtonGroup;
+ hButtons = buttonGroup.Children;
isMatch = strcmp({hButtons.Text}, value);
- hBtnGroup.SelectedObject = hButtons(isMatch);
+ buttonGroup.SelectedObject = hButtons(isMatch);
end
-
- function strPattern = getStrSearchPattern(obj, rowNumber, mode)
-
- if nargin < 3
- mode = obj.getStrSearchMode(rowNumber);
- end
-
+
+ function strPattern = getStringExtractPattern(obj, rowNumber)
+
hRow = obj.RowControls(rowNumber);
- strInd = hRow.StrfindInputEditbox.Value;
-
- strPattern = strInd;
- return
-
- switch lower(mode)
-
- case 'ind'
-% strInd = strrep(strInd, '-', ':');
-%
-% strInd = sprintf('[%s]', strInd);
-%
-% strPattern = eval(strInd);
-
- case 'expr'
- strPattern = strInd;
- end
+ inputValue = hRow.StringExtractInputField.Value;
+
+ strPattern = inputValue;
end
-
- function num = getSubfolderLevel(obj, rowNumber)
-
- hDropdown = obj.RowControls(rowNumber).FolderNameSelector;
-
- if strcmp(hDropdown.Value, 'Foldername not found') || ...
- strcmp(hDropdown.Value, 'Data location root folder not found')
- num = nan;
+
+ function indices = getSubfolderLevel(obj, rowNumber)
+ hRow = obj.RowControls(rowNumber);
+ if strcmp(hRow.FolderMultiSelector.Visible, 'on')
+ indices = hRow.FolderMultiSelector.UserData.SelectedIndices;
else
- items = hDropdown.Items(2:end); % Exclude first choice.
- num = find(strcmp(items, hDropdown.Value));
-
- % Note: important to exclude first entry. If no folder was
- % explicitly selected, the value of num should be empty.
- end
-
- % Todo: Make this more robust. Is it ever going to happen
- % unless the folder is not found like above?
- if numel( num ) > 1
- num = num(1);
- warning(['Multiple folders has the same name. Selected the first ' ...
- 'one in the list to use for metadata detection' ] )
+ folderItems = hRow.FolderNameSelector.UserData.FolderItems;
+ indices = find(strcmp(folderItems, hRow.FolderNameSelector.Value));
end
end
end
methods % Show/hide advanced options.
-
+
function createDataLocationSelector(obj, hPanel)
-
+
import uim.utility.layout.subdividePosition
-
+
toolbarPosition = obj.getToolbarPosition();
-
+
dataLocationLabelWidth = 110;
dataLocationSelectorWidth = 125;
- Wl_init = [dataLocationLabelWidth, dataLocationSelectorWidth];
-
+ requestedWidths = [dataLocationLabelWidth, dataLocationSelectorWidth];
+
% Get component positions for the components on the left
- [Xl, Wl] = subdividePosition(toolbarPosition(1), ...
- toolbarPosition(3), Wl_init, 10);
-
- Y = toolbarPosition(2);
-
+ [xPositions, adjustedWidths] = subdividePosition(toolbarPosition(1), ...
+ toolbarPosition(3), requestedWidths, 10);
+
+ yPosition = toolbarPosition(2);
+
% Create SelectDatalocationDropDownLabel
obj.SelectDatalocationDropDownLabel = uilabel(hPanel);
- obj.SelectDatalocationDropDownLabel.Position = [Xl(1) Y Wl(1) 22];
+ obj.SelectDatalocationDropDownLabel.Position = [xPositions(1) yPosition adjustedWidths(1) 22];
obj.SelectDatalocationDropDownLabel.Text = 'Select data location:';
% Create SelectDataLocationDropDown
obj.SelectDataLocationDropDown = uidropdown(hPanel);
obj.SelectDataLocationDropDown.Items = {'Rawdata'};
obj.SelectDataLocationDropDown.ValueChangedFcn = @obj.onDataLocationSelectionChanged;
- obj.SelectDataLocationDropDown.Position = [Xl(2) Y Wl(2) 22];
+ obj.SelectDataLocationDropDown.Position = [xPositions(2) yPosition adjustedWidths(2) 22];
obj.SelectDataLocationDropDown.Value = 'Rawdata';
-
+
obj.updateDataLocationSelector()
end
function createAdvancedOptionsButton(obj, hPanel)
%createAdvancedOptionsButton Create button to toggle advanced options
-
+
buttonSize = [160, 22];
-
+
toolbarPosition = obj.getToolbarPosition();
location(1) = sum(toolbarPosition([1,3])) - buttonSize(1);
location(2) = toolbarPosition(2);
@@ -765,13 +816,13 @@ function createAdvancedOptionsButton(obj, hPanel)
obj.AdvancedOptionsButton.Position = [location buttonSize];
obj.AdvancedOptionsButton.Text = 'Show Advanced Options...';
end
-
+
function onShowAdvancedOptionsButtonPushed(obj, src, ~)
%onShowAdvancedOptionsButtonPushed Button pushed callback
%
% Toggle the view for advanced options and update the button
% label according to button state
-
+
switch src.Text
case 'Show Advanced Options...'
obj.showAdvancedOptions()
@@ -781,9 +832,9 @@ function onShowAdvancedOptionsButtonPushed(obj, src, ~)
obj.AdvancedOptionsButton.Text = 'Show Advanced Options...';
end
end
-
+
function showAdvancedOptions(obj)
-
+
% Relocate / show header elements
obj.setColumnHeaderDisplayMode(true)
obj.IsAdvancedView = true;
@@ -793,12 +844,12 @@ function showAdvancedOptions(obj)
obj.setRowDisplayMode(i, true)
obj.setFunctionButtonVisibility(i)
end
-
+
drawnow
end
-
+
function hideAdvancedOptions(obj)
-
+
% Relocate / show header elements
obj.setColumnHeaderDisplayMode(false)
obj.IsAdvancedView = false;
@@ -808,26 +859,26 @@ function hideAdvancedOptions(obj)
obj.setRowDisplayMode(i, false)
obj.setFunctionButtonVisibility(i)
end
-
+
drawnow
end
-
+
function setColumnHeaderDisplayMode(obj, showAdvanced)
-
+
xOffset = sum(obj.ColumnWidths(4))+obj.ColumnSpacing;
visibility = 'off';
-
+
if showAdvanced
xOffset = -1 * xOffset;
visibility = 'on';
end
-
+
% Relocate / show header elements
obj.ColumnHeaderLabels{3}.Position(1) = obj.ColumnHeaderLabels{3}.Position(1) + xOffset;
obj.ColumnLabelHelpButton{3}.Position(1) = obj.ColumnLabelHelpButton{3}.Position(1) + xOffset;
obj.ColumnHeaderLabels{4}.Visible = visibility;
obj.ColumnLabelHelpButton{4}.Visible = visibility;
-
+
if showAdvanced
obj.ColumnHeaderLabels{3}.Text = 'Selection mode';
obj.ColumnLabelHelpButton{3}.Tag = 'Selection mode';
@@ -836,51 +887,56 @@ function setColumnHeaderDisplayMode(obj, showAdvanced)
obj.ColumnLabelHelpButton{3}.Tag = 'Select string';
end
end
-
- function setRowDisplayMode(obj, rowNum, showAdvanced)
-
+
+ function setRowDisplayMode(obj, rowNumber, showAdvanced)
+
xOffset = sum(obj.ColumnWidths(4))+obj.ColumnSpacing;
visibility = 'off';
visibility_ = 'on';
-
+
if showAdvanced
xOffset = -1 * xOffset;
visibility = 'on';
visibility_ = 'off';
end
-
- hRow = obj.RowControls(rowNum);
+
+ hRow = obj.RowControls(rowNumber);
hRow.FolderNameSelector.Position(3) = hRow.FolderNameSelector.Position(3) + xOffset;
+ hRow.FolderMultiSelector.Position(3) = hRow.FolderMultiSelector.Position(3) + xOffset;
+ obj.updateFolderSelectorButtonText( ...
+ hRow.FolderMultiSelector, ...
+ hRow.FolderMultiSelector.UserData.FolderItems, ...
+ hRow.FolderMultiSelector.UserData.SelectedIndices);
hRow.SelectSubstringButton.Position(1) = hRow.SelectSubstringButton.Position(1) + xOffset;
hRow.SelectSubstringButton.Visible = visibility_;
- hRow.ButtonGroupStrfindMode.Visible = visibility;
- hRow.StrfindInputEditbox.Visible = visibility;
+ hRow.StringExtractModeButtonGroup.Visible = visibility;
+ hRow.StringExtractInputField.Visible = visibility;
end
-
+
function setFunctionButtonVisibility(obj, rowNumber)
-
+
hRow = obj.RowControls(rowNumber);
-
- showButtons = strcmp(hRow.ButtonGroupStrfindMode.SelectedObject.Text, 'func') ...
+
+ showButtons = strcmp(hRow.StringExtractModeButtonGroup.SelectedObject.Text, 'func') ...
&& obj.IsAdvancedView;
if showButtons
hRow.RunFunctionButton.Visible = 'on';
hRow.EditFunctionButton.Visible = 'on';
- hRow.StrfindInputEditbox.Visible = 'off';
+ hRow.StringExtractInputField.Visible = 'off';
else
hRow.RunFunctionButton.Visible = 'off';
hRow.EditFunctionButton.Visible = 'off';
if obj.IsAdvancedView
- hRow.StrfindInputEditbox.Visible = 'on';
+ hRow.StringExtractInputField.Visible = 'on';
else
- hRow.StrfindInputEditbox.Visible = 'off';
+ hRow.StringExtractInputField.Visible = 'off';
end
end
end
end
-
+
methods (Access = protected) % Listener callbacks inherited from HasDataLocationModel
function onDataLocationModified(obj, ~, evt)
@@ -889,15 +945,15 @@ function onDataLocationModified(obj, ~, evt)
% This method is inherited from the HasDataLocationModel
% superclass and is triggered by the DataLocationModified event
% on the DataLocationModel object
-
+
switch evt.DataField
case 'SubfolderStructure'
-
+
% Todo: Should this be more specific? i.e does not need
% to invoke this method know when filters change...
-
+
[~, idx] = obj.DataLocationModel.containsItem(evt.DataLocationName);
-
+
% Currently, only the first data location requires an
% update of this ui.
if idx == obj.DataLocationIndex
@@ -907,8 +963,7 @@ function onDataLocationModified(obj, ~, evt)
% Update result of string indexing based on model...
for i = 1:obj.NumRows
- hComp = obj.RowControls(i).StrfindInputEditbox;
- obj.onStringInputValueChanged(hComp)
+ obj.refreshStringResult(i)
end
end
case 'Name'
@@ -918,41 +973,194 @@ function onDataLocationModified(obj, ~, evt)
% No change is necessary
end
end
-
- function onDataLocationAdded(obj, ~, evt)
+
+ function onDataLocationAdded(obj, ~, ~)
%onDataLocationAdded Callback for DataLocationModel event
%
% This method is inherited from the HasDataLocationModel
% superclass and is triggered by the DataLocationAdded event on
% the DataLocationModel object
-
+
obj.updateDataLocationSelector()
end
-
- function onDataLocationRemoved(obj, ~, evt)
+
+ function onDataLocationRemoved(obj, ~, ~)
%onDataLocationRemoved Callback for DataLocationModel event
%
% This method is inherited from the HasDataLocationModel
% superclass and is triggered by the DataLocationRemoved event on
% the DataLocationModel object
-
+
obj.updateDataLocationSelector()
end
end
-
+
methods (Access = private)
+ function alertFunctionNotFound(obj, rowNumber)
+ %alertFunctionNotFound Show a uialert when the extraction function is missing
+ hFig = ancestor(obj.RowControls(rowNumber).VariableName, 'figure');
+ variableName = obj.RowControls(rowNumber).VariableName.Text;
+ message = sprintf(['A function for extracting "%s" does not exist yet. ' ...
+ 'Please press "Edit" to initialize the function from a template.'], variableName);
+ uialert(hFig, message, 'Function missing...', 'Icon', 'info')
+ end
+
function substring = getSubstringFromRowFunction(obj, rowNumber)
- dlIdx = obj.DataLocationIndex;
- thisDataLocation = obj.DataLocationModel.Data(dlIdx);
- pathStr = thisDataLocation.ExamplePath;
+ dataLocationIndex = obj.DataLocationIndex;
+ thisDataLocation = obj.DataLocationModel.Data(dataLocationIndex);
+ folderPath = thisDataLocation.ExamplePath;
dataLocationName = thisDataLocation.Name;
- substring = feval(obj.FunctionName{rowNumber}, pathStr, dataLocationName);
+ substring = feval(obj.FunctionName{rowNumber}, folderPath, dataLocationName);
+ end
+
+ function exitFuncModeIfActive(obj, rowNumber)
+ %exitFuncModeIfActive Switch from func to ind mode when folder selection changes
+ %
+ % func mode ignores folder selection entirely, so keeping it active
+ % when the user changes the folder would produce a silent no-op.
+ % Switching back to ind is the predictable default; the user can
+ % re-select expr or func manually if needed.
+
+ if strcmp(obj.getStringExtractMode(rowNumber), 'func')
+ obj.setStringExtractMode(rowNumber, 'ind')
+ obj.setFunctionButtonVisibility(rowNumber)
+ end
+ end
+
+ function switchToMultiSelectMode(obj, rowNumber, selectedIndices, separator)
+ %switchToMultiSelectMode Show button, hide dropdown, store selection
+
+ hRow = obj.RowControls(rowNumber);
+ folderItems = hRow.FolderNameSelector.UserData.FolderItems;
+
+ hRow.FolderMultiSelector.UserData.FolderItems = folderItems;
+ hRow.FolderMultiSelector.UserData.SelectedIndices = selectedIndices;
+ obj.Separator{rowNumber} = separator;
+
+ obj.updateFolderSelectorButtonText( ...
+ hRow.FolderMultiSelector, folderItems, selectedIndices);
+
+ hRow.FolderNameSelector.Visible = 'off';
+ hRow.FolderMultiSelector.Visible = 'on';
+ end
+
+ function switchToSingleSelectMode(obj, rowNumber, selectedIndex)
+ %switchToSingleSelectMode Hide button, show dropdown, clear multi-selection
+
+ hRow = obj.RowControls(rowNumber);
+ folderItems = hRow.FolderNameSelector.UserData.FolderItems;
+
+ hRow.FolderMultiSelector.Visible = 'off';
+ hRow.FolderMultiSelector.UserData.SelectedIndices = [];
+ obj.Separator{rowNumber} = '';
+
+ hRow.FolderNameSelector.Visible = 'on';
+
+ if ~isempty(selectedIndex) && selectedIndex >= 1 && selectedIndex <= numel(folderItems)
+ hRow.FolderNameSelector.Value = folderItems{selectedIndex};
+ else
+ hRow.FolderNameSelector.Value = hRow.FolderNameSelector.Items{1};
+ end
+ end
+
+ function combinedName = getCombinedFolderName(obj, rowNumber)
+ %getCombinedFolderName Get the combined folder string for a row
+ %
+ % Used by onSelectSubstringButtonPushed to show the user the
+ % string that the extraction pattern will be applied to.
+
+ dataLocationIndex = obj.DataLocationIndex;
+ thisDataLocation = obj.DataLocationModel.Data(dataLocationIndex);
+ separator = obj.Separator{rowNumber};
+
+ combinedName = nansen.config.dloc.DataLocationModel.combineFolderNamesFromPath( ...
+ thisDataLocation.ExamplePath, ...
+ obj.getSubfolderLevel(rowNumber), ...
+ numel(thisDataLocation.SubfolderStructure), ...
+ separator);
+ end
+
+ function updateFolderSelectorButtonText(~, hButton, folderItems, selectedIndices)
+ %updateFolderSelectorButtonText Update button label and tooltip to reflect selection
+
+ if isempty(selectedIndices)
+ hButton.Text = 'Select folder(s)...';
+ hButton.Tooltip = '';
+ else
+ arrowIndicator = ' ▼';
+ fullName = strjoin(folderItems(selectedIndices), ' + ');
+ label = truncateTextForWidth(fullName, hButton.Position(3), ...
+ hButton.FontSize, arrowIndicator);
+ hButton.Text = [label arrowIndicator];
+ hButton.Tooltip = fullName;
+ end
+ end
+
+ function [selectedIndices, separator] = showFolderSelectorDialog( ...
+ ~, folderItems, currentIndices, currentSeparator, parentPosition)
+ % showFolderSelectorDialog Modal dialog for selecting folder levels
+ %
+ % Opens a figure with a multi-select listbox and a separator
+ % field. Returns the selected indices and separator string, or
+ % the original values if the user cancels.
+
+ selectedIndices = currentIndices;
+ separator = currentSeparator;
+
+ dialogWidth = 300;
+ dialogHeight = 300;
+ dialogX = parentPosition(1) + (parentPosition(3) - dialogWidth) / 2;
+ dialogY = parentPosition(2) + (parentPosition(4) - dialogHeight) / 2;
+
+ dialogFigure = uifigure( ...
+ 'Name', 'Select Folder Level(s)', ...
+ 'Position', [dialogX, dialogY, dialogWidth, dialogHeight], ...
+ 'WindowStyle', 'modal', ...
+ 'Resize', 'off');
+
+ uilabel(dialogFigure, ...
+ 'Text', 'Select one or more folder levels:', ...
+ 'Position', [15 265 270 22]);
+
+ hListbox = uilistbox(dialogFigure, ...
+ 'Items', folderItems, ...
+ 'Multiselect', 'on', ...
+ 'Position', [15 110 270 150]);
+
+ if ~isempty(currentIndices) && max(currentIndices) <= numel(folderItems)
+ hListbox.Value = folderItems(currentIndices);
+ end
+
+ uilabel(dialogFigure, 'Text', 'Separator:', 'Position', [15 75 80 22]);
+ hSeparatorField = uieditfield(dialogFigure, 'text', ...
+ 'Value', currentSeparator, ...
+ 'Position', [100 75 185 22], ...
+ 'Placeholder', 'e.g. _ (leave blank for none)');
+
+ uibutton(dialogFigure, 'Text', 'OK', ...
+ 'Position', [195 30 90 30], ...
+ 'ButtonPushedFcn', @(~,~) uiresume(dialogFigure));
+ uibutton(dialogFigure, 'Text', 'Cancel', ...
+ 'Position', [100 30 90 30], ...
+ 'ButtonPushedFcn', @(~,~) delete(dialogFigure));
+
+ uiwait(dialogFigure);
+
+ % If figure still exists, the user pressed OK — read the values.
+ if isvalid(dialogFigure)
+ selected = hListbox.Value;
+ if ischar(selected); selected = {selected}; end
+ selectedIndices = find(ismember(folderItems, selected));
+ separator = hSeparatorField.Value;
+ delete(dialogFigure);
+ end
+ % else: user cancelled or closed — return the original values.
end
end
methods (Static, Access = private)
-
- function identifierName = getIdNameForRow(rowNumber)
+
+ function identifierName = getIdentifierNameForRow(rowNumber)
identifierNames = {'subjectId', 'sessionId', 'experimentDate', 'experimentTime'};
identifierName = identifierNames{rowNumber};
end
@@ -960,10 +1168,10 @@ function onDataLocationRemoved(obj, ~, evt)
function tf = isDateTimeVariable(variableName)
tf = contains(variableName, {'Date', 'Time'});
end
-
+
function [inFormat, outFormat] = uiGetDateTimeFormat(variableName, strValue)
%uiGetDateTimeFormat Get datetime input and output format
-
+
% Get datetime values for date & time variables.
if strcmp(variableName, 'Experiment Date')
dlgTitle = 'Enter Date Format';
@@ -974,10 +1182,10 @@ function onDataLocationRemoved(obj, ~, evt)
msg = sprintf('Please enter time format for the selected text: "%s". For example: HH-mm-ss.', strValue);
outFormat = 'HH:mm:ss';
end
-
+
msg = strjoin({msg, 'See the MATLAB documentation for "datetime" for a full list of examples (type ''doc datetime'' in MATLAB''s Command Window).'});
answer = inputdlg(msg, dlgTitle);
-
+
if ~isempty(answer) && ~isempty(answer{1})
inFormat = answer{1};
else
@@ -986,50 +1194,50 @@ function onDataLocationRemoved(obj, ~, evt)
end
function outFormat = getDateTimeOutFormat(variableName)
-
+
if strcmp(variableName, 'Experiment Date')
outFormat = 'MMM-dd-yyyy';
elseif strcmp(variableName, 'Experiment Time')
outFormat = 'HH:mm:ss';
end
end
-
- function IND = simplifyInd(IND)
- %simplifyInd Simplify the indices, by joining all subsequent using
- %the colon separator, i.e 1 2 3 4 5 -> 1:5
-
- indOrig = num2str(IND);
-
- indNew = {};
+
+ function indices = simplifyIndices(indices)
+ %simplifyIndices Simplify the indices, by joining all subsequent using
+ % the colon separator, i.e 1 2 3 4 5 -> 1:5
+
+ originalString = num2str(indices);
+
+ simplifiedParts = {};
count = 1;
-
+
finished = false;
while ~finished
-
+
% Find number in list which is not increment of previous
- lastSequenceIdx = find(diff(IND, 2) ~= 0, 1, 'first') + 1;
- if isempty(lastSequenceIdx)
- lastSequenceIdx = numel(IND);
+ lastSequenceIndex = find(diff(indices, 2) ~= 0, 1, 'first') + 1;
+ if isempty(lastSequenceIndex)
+ lastSequenceIndex = numel(indices);
end
-
+
% Add indices of format first:last to results
- indNew{count} = sprintf('%d:%d', IND(1), IND(lastSequenceIdx));
+ simplifiedParts{count} = sprintf('%d:%d', indices(1), indices(lastSequenceIndex)); %#ok We don't know beforehand how long this array will be
% Remove all numbers that were part of sequence
- IND(1:lastSequenceIdx) = [];
+ indices(1:lastSequenceIndex) = [];
count = count+1;
- if isempty(IND)
+ if isempty(indices)
finished = true;
end
end
% Join sequences
- IND = strjoin(indNew, ',');
-
+ indices = strjoin(simplifiedParts, ',');
+
% Keep the shortest character vector
- if numel(IND) > indOrig
- IND = indOrig;
+ if numel(indices) > originalString
+ indices = originalString;
end
end
@@ -1041,9 +1249,27 @@ function onDataLocationRemoved(obj, ~, evt)
end
end
+function truncatedText = truncateTextForWidth(text, widthPx, fontSizePt, ~)
+%truncateTextForWidth Truncate text so it fits within a pixel width budget
+%
+% Estimates character width as 0.6x the font size (pt→px, proportional
+% font approximation) and reserves a fixed pixel budget for the arrow
+% indicator. Replaces the last character with '…' when truncation occurs.
+
+ arrowReservedPx = 16;
+ pixelsPerChar = fontSizePt * 0.6;
+ maxChars = floor((widthPx - arrowReservedPx) / pixelsPerChar);
+
+ if numel(text) > maxChars && maxChars > 3
+ truncatedText = [text(1:maxChars-1) '…'];
+ else
+ truncatedText = text;
+ end
+end
+
function functionName = createFunctionName(identifierName)
%Example: subjectId -> getSubjectId
-
+
identifierName(1) = upper(identifierName(1));
functionName = sprintf('get%s', identifierName);
end