diff --git a/@MyLog/MyLog.m b/@MyLog/MyLog.m index 3eaac00..532f369 100644 --- a/@MyLog/MyLog.m +++ b/@MyLog/MyLog.m @@ -1,734 +1,734 @@ % Class to store data versus time. % Data can be continuously appended and saved. It is possible to add % labels (time marks) for particular moments in time. Data can be saved % and plotted with the time marks. % Metadata for this class is stored independently. % If instantiated as MyLog(load_path) then % the content is loaded from file classdef MyLog < matlab.mixin.Copyable properties (Access = public) % Save time as posixtime up to ms precision time_fmt = '%14.3f' % Save data as reals with up to 14 decimal digits. Trailing zeros % are removed by %g data_fmt = '%.14g' % Format for displaying the last reading (column name: value) disp_fmt = '%15s: %.3g' % Data column and line separators column_sep = '\t' line_sep = '\r\n' % File extension that is appended by default when saving the log % if a different one is not specified explicitly data_file_ext = '.log' % File extension for metadata meta_file_ext = '.meta' % Formatting options for the metadata metadata_opts = {} file_name = '' % Used to save or load the data data_headers = {} % Cell array of column headers length_lim = Inf % Keep the log length below this limit % Format for string representation of timestamps datetime_fmt = 'yyyy-MMM-dd HH:mm:ss' end properties (SetAccess = public, GetAccess = public) timestamps % Times at which data was aqcuired data % Array of measurements % Structure array that stores labeled time marks TimeLabels = struct( ... 'time', {}, ... % datetime object 'time_str', {}, ... % time in text format 'text_str', {}); % message string % Structure array that stores all the axes the log is plotted in PlotList = struct( ... 'Axes', {}, ... % axes handles 'DataLines',{}, ... % data line handles 'LbLines', {}, ... % labels line handles 'LbText', {}); % labels text handles end properties (Dependent = true) data_line_fmt % Format specifier for one data row to be printed column_headers % Time column header + data column headers data_file_name % File name with extension for data saving meta_file_name % File name with extension for metadata saving timestamps_num % timestamps converted to numeric format end methods (Access = public) function this = MyLog(varargin) P = MyClassParser(this); processInputs(P, this, varargin{:}); end %% Save and load functionality % save the entire data record function save(this, filename) % Verify that the data can be saved assertDataMatrix(this); % File name can be either supplied explicitly or given as the % file_name property if nargin() < 2 filename = this.file_name; else this.file_name = filename; end assert(~isempty(filename), 'File name is not provided.'); datfname = this.data_file_name; metfname = this.meta_file_name; stat = createFile(datfname); if ~stat return end % Save time labels in a separate file, creating or clearing % it first Mdt = getMetadata(this); fid = fopen(metfname, 'w'); fclose(fid); save(Mdt, metfname); fid = fopen(datfname,'w'); % Write column headers str = printDataHeaders(this); fprintf(fid, '%s', str); % Write data body fmt = this.data_line_fmt; for i = 1:length(this.timestamps) fprintf(fid, fmt, this.timestamps_num(i), this.data(i,:)); end fclose(fid); end %% Plotting % Plot the log data with time labels. Reurns plotted line objects. function Pls = plot(this, varargin) % Verify that the data is a numeric matrix, % otherwise it cannot be plotted assertDataMatrix(this); [~, ncols] = size(this.data); p=inputParser(); % Axes in which log should be plotted addOptional(p, 'Ax', [], @(x)assert( ... isa(x,'matlab.graphics.axis.Axes')||... isa(x,'matlab.ui.control.UIAxes'),... 'Argument must be axes or uiaxes.')); % If time labels are to be displayed addParameter(p, 'time_labels', true, @islogical); % If legend is to be displayed addParameter(p, 'legend', true, @islogical); % Logical vector defining the data columns to be displayed addParameter(p, 'isdisp', true(1,ncols), @(x) assert(... islogical(x) && isvector(x) && length(x)==ncols, ... ['''isdisp'' must be a logical vector of the size ',... 'equal to the number of data columns.'])); % If 'reset' is true than all the data lines and time labels are % re-plotted with default style even if they are already present % in the plot addParameter(p, 'reset', false, @islogical); parse(p, varargin{:}); if ~isempty(p.Results.Ax) Ax=p.Results.Ax; else Ax=gca(); end % Find out if the log was already plotted in these axes. If % not, appned Ax to the PlotList. ind=findPlotInd(this, Ax); if isempty(ind) l=length(this.PlotList); this.PlotList(l+1).Axes=Ax; ind=l+1; end % Plot data if isempty(this.PlotList(ind).DataLines) % If the log was never plotted in Ax, % plot using default style and store the line handles Pls=line(Ax, this.timestamps, this.data); this.PlotList(ind).DataLines=Pls; else % Replace existing data Pls=this.PlotList(ind).DataLines; for i=1:length(Pls) try Pls(i).XData=this.timestamps; Pls(i).YData=this.data(:,i); catch warning(['Could not update plot for '... '%i-th data column'],i); end end end % Set the visibility of lines if ~ismember('isdisp',p.UsingDefaults) for i=1:ncols Pls(i).Visible=p.Results.isdisp(i); end end % Plot time labels and legend if (p.Results.time_labels) plotTimeLabels(this, Ax); end if (p.Results.legend)&&(~isempty(this.data_headers))&&... (~isempty(this.data)) % Add legend only for for those lines that are displayed disp_ind = cellfun(@(x)strcmpi(x,'on'),{Pls.Visible}); legend(Ax, Pls(disp_ind), this.data_headers{disp_ind},... 'Location','southwest'); end end %% Manipulations with log data % Append data point to the log function appendData(this, time, val, varargin) p = inputParser(); addParameter(p, 'save', false, @islogical); parse(p, varargin{:}); % Format checks on the input data assert(isa(time,'datetime')||isnumeric(time),... ['''time'' argument must be numeric or ',... 'of the class datetime.']); assert(isrow(val),'''val'' argument must be a row vector.'); if ~isempty(this.data) - [~, ncols]=size(this.data); - assert(length(val)==ncols,['Length of ''val'' ' ... + [~, ncols] = size(this.data); + assert(length(val) == ncols,['Length of ''val'' ' ... 'does not match the number of data columns']); end % Ensure time format if isa(time,'datetime') time.Format = this.datetime_fmt; end % Append new data and time stamps this.timestamps = [this.timestamps; time]; this.data = [this.data; val]; % Ensure the log length is within the length limit trim(this); % Optionally save the new data point to file if p.Results.save try exstat = exist(this.data_file_name, 'file'); if exstat == 0 % If the file does not exist, create it and write % the column headers createFile(this.data_file_name); fid = fopen(this.data_file_name,'w'); str = printDataHeaders(this); fprintf(fid,'%s',str); else % Otherwise open for appending fid = fopen(this.data_file_name,'a'); end % Convert the new timestamps to numeric form for saving if isa(time,'datetime') time_num = posixtime(time); else time_num = time; end % Append new data points to file fprintf(fid, this.data_line_fmt, time_num, val); fclose(fid); % Save metadata with time labels if ~isempty(this.TimeLabels) && ... exist(this.meta_file_name, 'file')==0 save(this.Metadata, this.meta_file_name, ... 'overwrite', true); end catch warning(['Logger cannot save data at time = ',... datestr(datetime('now', ... 'Format',this.datetime_fmt))]); % Try closing fid in case it is still open try fclose(fid); catch end end end end %% Time labels function plotTimeLabels(this, Ax) % Find out if the log was already plotted in these axes ind=findPlotInd(this, Ax); if isempty(ind) l=length(this.PlotList); this.PlotList(l+1).Axes=Ax; ind=l+1; end % Remove existing labels eraseTimeLabels(this, Ax); % Define marker lines to span over the entire plot ymin=Ax.YLim(1); ymax=Ax.YLim(2); markline = linspace(ymin, ymax, 2); % Plot labels for i=1:length(this.TimeLabels) T=this.TimeLabels(i); marktime = [T.time,T.time]; % Add text label to plot, with 5% offset from % the boundary for beauty Txt=text(Ax, T.time, ymin+0.95*(ymax-ymin), T.text_str,... 'Units','data',... 'HorizontalAlignment','right',... 'VerticalAlignment','top',... 'FontWeight', 'bold',... 'Rotation',90,... 'BackgroundColor','white',... 'Clipping','on',... 'Margin',1); % Add line to plot Pl=line(Ax, marktime, markline,'color','black'); % Store the handles of text and line this.PlotList(ind).LbLines = ... [this.PlotList(ind).LbLines,Pl]; this.PlotList(ind).LbText = ... [this.PlotList(ind).LbText,Txt]; end end % Remove existing labels from the plot function eraseTimeLabels(this, Ax) % Find out if the log was already plotted in these axes ind=findPlotInd(this, Ax); if ~isempty(ind) % Remove existing labels delete(this.PlotList(ind).LbLines); this.PlotList(ind).LbLines=[]; delete(this.PlotList(ind).LbText); this.PlotList(ind).LbText=[]; else warning('Cannot erase time labels. Axes not found.') end end % Add label % Form with optional arguments: addTimeLabel(this, time, str) function addTimeLabel(this, varargin) p=inputParser(); addOptional(p, 'time', ... datetime('now', 'Format', this.datetime_fmt), ... @(x)assert(isa(x,'datetime'), ... '''time'' must be of the type datetime.')); addOptional(p, 'str', '', ... @(x) assert(iscellstr(x)||ischar(x)||isstring(x), ... '''str'' must be a string or cell array of strings.')); addParameter(p, 'save', false, @islogical); parse(p, varargin{:}); if any(ismember({'time','str'}, p.UsingDefaults)) % Invoke a dialog to add the label time and name answ = inputdlg({'Label text', 'Time'},'Add time label',... [2 40; 1 40],{'',datestr(p.Results.time)}); if isempty(answ)||isempty(answ{1}) return else % Conversion of the inputed value to datetime to % ensure proper format time=datetime(answ{2}, 'Format', this.datetime_fmt); % Store multiple lines as cell array str=cellstr(answ{1}); end end % Need to calculate length explicitly as using 'end' fails % for an empty array l=length(this.TimeLabels); this.TimeLabels(l+1).time=time; this.TimeLabels(l+1).time_str=datestr(time); this.TimeLabels(l+1).text_str=str; % Order time labels by ascending time sortTimeLabels(this); if p.Results.save==true % Save metadata with new time labels save(this.Metadata, this.meta_file_name, ... 'overwrite', true); end end % Modify text or time of an exising label. If new time and text are % not provided as arguments, modifyTimeLabel(this, ind, time, str), % invoke a dialog. % ind - index of the label to be modified in TimeLabels array. function modifyTimeLabel(this, ind, varargin) p=inputParser(); addRequired(p, 'ind', @(x)assert((rem(x,1)==0)&&(x>0), ... '''ind'' must be a positive integer.')); addOptional(p, 'time', ... datetime('now', 'Format', this.datetime_fmt), ... @(x)assert(isa(x,'datetime'), ... '''time'' must be of the type datetime.')); addOptional(p, 'str', '', ... @(x) assert(iscellstr(x)||ischar(x)||isstring(x), ... '''str'' must be a string or cell array of strings.')); addParameter(p, 'save', false, @islogical); parse(p, ind, varargin{:}); if any(ismember({'time','str'}, p.UsingDefaults)) Tlb=this.TimeLabels(ind); answ = inputdlg({'Label text', 'Time'},'Modify time label',... [2 40; 1 40],{char(Tlb.text_str), Tlb.time_str}); if isempty(answ)||isempty(answ{1}) return else % Convert the input value to datetime and ensure % proper format time=datetime(answ{2}, 'Format', this.datetime_fmt); % Store multiple lines as cell array str=cellstr(answ{1}); end end this.TimeLabels(ind).time=time; this.TimeLabels(ind).time_str=datestr(time); this.TimeLabels(ind).text_str=str; % Order time labels by ascending time sortTimeLabels(this); if p.Results.save==true % Save metadata with new time labels save(this.Metadata, this.meta_file_name, ... 'overwrite', true); end end % Show the list of labels in readable format function lst=printTimeLabelList(this) lst=cell(length(this.TimeLabels),1); for i=1:length(this.TimeLabels) if ischar(this.TimeLabels(i).text_str) ||... isstring(this.TimeLabels(i).text_str) tmpstr=this.TimeLabels(i).text_str; elseif iscell(this.TimeLabels(i).text_str) % If text is cell array, elements corresponding to % multiple lines, display the first line tmpstr=this.TimeLabels(i).text_str{1}; end lst{i}=[this.TimeLabels(i).time_str,' ', tmpstr]; end end %% Misc public functions % Clear log data and time labels function clear(this) % Clear while preserving the array types this.TimeLabels(:)=[]; % Delete all the data lines and time labels for i=1:length(this.PlotList) delete(this.PlotList(i).DataLines); delete(this.PlotList(i).LbLines); delete(this.PlotList(i).LbText); end this.PlotList(:)=[]; % Clear data and its type this.timestamps = []; this.data = []; end % Verify that the data can be saved or plotted function assertDataMatrix(this) assert(ismatrix(this.data)&&isnumeric(this.data),... ['Data is not a numeric matrix, saving in '... 'text format is not possible.']); end % Display last reading function str = printLastReading(this) if isempty(this.timestamps) str = ''; else str = ['Last reading ',char(this.timestamps(end)),newline]; last_data = this.data(end,:); for i=1:length(last_data) if length(this.data_headers)>=i lbl = this.data_headers{i}; else lbl = sprintf('data%i',i); end str = [str,... sprintf(this.disp_fmt,lbl,last_data(i)),newline]; %#ok end end end end methods (Access = public, Static = true) % Load log from file. Formatting parameters can be supplied as % varargin function L = load(filename, varargin) assert(exist(filename, 'file') == 2, ... ['File ''', filename, ''' is not found.']) L = MyLog(varargin{:}); L.file_name = filename; % Load metadata if file is found if exist(L.meta_file_name, 'file') == 2 Mdt = MyMetadata.load(L.meta_file_name,L.metadata_opts{:}); setMetadata(L, Mdt); else disp(['Log metadata file is not found, continuing ' ... 'without it.']); end % Read column headers from the data file fid = fopen(filename,'r'); dat_col_heads = strsplit(fgetl(fid), L.column_sep, ... 'CollapseDelimiters', true); fclose(fid); % Assign column headers first, prioritizing those found in % the metadata file over those found in the main file. This is % done because the column names in the main file are not % updated once they are printed, while the column names in % metadata are always up to date. % Read data as delimiter-separated values and convert to cell % array, skip the first line containing column headers fulldata = dlmread(filename, L.column_sep, 1, 0); L.data = fulldata(:,2:end); L.timestamps = fulldata(:,1); % Convert time stamps to datetime if the time column header % is 'posixtime' if ~isempty(dat_col_heads) && ... contains(dat_col_heads{1}, 'posix', 'IgnoreCase', true) L.timestamps = datetime(L.timestamps, ... 'ConvertFrom', 'posixtime', 'Format', L.datetime_fmt); end end end methods (Access = protected) %% Auxiliary private functions % Ensure the log length is within length limit function trim(this) l = length(this.timestamps); if l > this.length_lim dn = l-this.length_lim; this.timestamps(1:dn) = []; this.data(1:dn) = []; end end % Print column names to a string function str = printDataHeaders(this) cs = this.column_sep; str = sprintf(['%s',cs], this.column_headers{:}); str = [str, sprintf(this.line_sep)]; end % Find out if the log was already plotted in the axes Ax and return % the corresponding index of PlotList if it was function ind = findPlotInd(this, Ax) assert(isvalid(Ax),'Ax must be valid axes or uiaxes') if ~isempty(this.PlotList) ind_b=cellfun(@(x) isequal(x, Ax),{this.PlotList.Axes}); % Find index of the first match ind=find(ind_b,1); else ind=[]; end end % Re-order the elements of TimeLabels array so that labels % corresponding to later times have larger index function sortTimeLabels(this) times = [this.TimeLabels.time]; [~,ind] = sort(times); this.TimeLabels = this.TimeLabels(ind); end % Create metadata from log properties function Mdt = getMetadata(this) % Add column names CnMdt = MyMetadata(this.metadata_opts{:}, ... 'title', 'ColumnNames'); addParam(CnMdt, 'Name', this.column_headers); if ~isempty(this.TimeLabels) % Add the textual part of TimeLabels structure TlMdt = MyMetadata(this.metadata_opts{:}, ... 'title', 'TimeLabels'); Lbl = struct('time_str', {this.TimeLabels.time_str}, ... 'text_str', {this.TimeLabels.text_str}); addParam(TlMdt, 'TimeLabels', 'Lbl', Lbl); else TlMdt = MyMetadata.empty(); end Mdt = [CnMdt, TlMdt]; end % Process metadata function setMetadata(this, Mdt) % Assign column names if ismember('ColumnNames', Mdt.field_names) && ... length(Mdt.ColumnNames.Name.value)>=2 % Assign column headers from metadata if present this.data_headers=Mdt.ColumnNames.Name.value(2:end); elseif length(dat_col_heads)>=2 this.data_headers=dat_col_heads(2:end); end % Assign time labels if ismember('TimeLabels', Mdt.field_names) Lbl=Mdt.TimeLabels.Lbl.value; for i=1:length(Lbl) this.TimeLabels(i).time_str=Lbl(i).time_str; this.TimeLabels(i).time=datetime(Lbl(i).time_str, ... 'Format', this.datetime_fmt); this.TimeLabels(i).text_str=Lbl(i).text_str; end end end end %% set and get methods methods function set.length_lim(this, val) assert(isreal(val),'''length_lim'' must be a real number'); % Make length_lim non-negative and integer this.length_lim=max(0, round(val)); % Apply the new length limit to log trim(this); end function set.data_headers(this, val) assert(iscellstr(val) && isrow(val), ['''data_headers'' must '... 'be a row cell array of character strings.']) %#ok this.data_headers=val; end % The get function for file_name adds extension if it is not % already present and also ensures proper file separators % (by splitting and combining the file name) function fname = get.data_file_name(this) fname = this.file_name; [filepath,name,ext] = fileparts(fname); if isempty(ext) ext = this.data_file_ext; end fname = fullfile(filepath,[name,ext]); end function fname = get.meta_file_name(this) fname = this.file_name; [filepath,name,~] = fileparts(fname); ext=this.meta_file_ext; fname = fullfile(filepath,[name,ext]); end function data_line_fmt=get.data_line_fmt(this) cs=this.column_sep; nl=this.line_sep; if isempty(this.data) l=0; else [~,l]=size(this.data); end data_line_fmt = this.time_fmt; for i=1:l data_line_fmt = [data_line_fmt, cs, this.data_fmt]; %#ok end data_line_fmt = [data_line_fmt, nl]; end function hdrs=get.column_headers(this) % Add header for the time column if isa(this.timestamps,'datetime') time_title_str = 'POSIX time (s)'; else time_title_str = 'Time'; end hdrs=[time_title_str,this.data_headers]; end function time_num_arr=get.timestamps_num(this) % Convert time stamps to numbers if isa(this.timestamps,'datetime') time_num_arr=posixtime(this.timestamps); else time_num_arr=this.timestamps; end end end end diff --git a/@MyMetadata/MyMetadata.m b/@MyMetadata/MyMetadata.m index cdcd4be..a6dfa57 100644 --- a/@MyMetadata/MyMetadata.m +++ b/@MyMetadata/MyMetadata.m @@ -1,440 +1,436 @@ % MyMetadata stores parameter-value pairs and can be saved in a readable % format. % % Metadata parameters can be strings, numerical values or cells, as well as % any arrays and structures of such with arbitrary nesting. Sub-indices are % automatically expanded when saving. classdef MyMetadata < handle & matlab.mixin.CustomDisplay & ... matlab.mixin.SetGet & matlab.mixin.Copyable properties (Access = public) % Header sections are separated by [hdr_spec, title, hdr_spec] title = '' hdr_spec = '==' % Columns are separated by this symbol (space-tab by default) column_sep = ' \t' % Comments start from this symbol comment_sep = '%' line_sep = '\r\n' % Limit for column padding. Variables which take more space than % this limit are ignored when calculating the padding length. pad_lim = 15 end properties (GetAccess = public, SetAccess = protected) ParamList = struct() % Values of parameters ParamOptList = struct() % Options for parameters end methods (Access = public) function this = MyMetadata(varargin) P = MyClassParser(this); processInputs(P, this, varargin{:}); end % Adds a new metadata parameter. function addParam(this, param_name, value, varargin) assert(isvarname(param_name), ['Parameter name must be a ' ... 'valid variable name.']); p = inputParser(); % Format specifier for printing the value addParameter(p, 'fmt_spec', '', @ischar); % Comment to be added to the line addParameter(p, 'comment', '', @ischar); addParameter(p, 'SubStruct', struct('type',{},'subs',{}),... @isstruct) parse(p, varargin{:}); % Make sure that the comment does not contain new line or % carriage return characters, which would mess up formating % when saving the metadata [comment, is_mod] = toSingleLine(p.Results.comment); this.ParamOptList.(param_name).comment = comment; if is_mod warning(['Comment string for ''%s'' has been ' ... 'converted to single line.'], param_name); end this.ParamOptList.(param_name).fmt_spec = p.Results.fmt_spec; S = p.Results.SubStruct; if isempty(S) this.ParamList.(param_name) = value; else % Initialize property with an empty variable of the same % class as value, this is important if SubStruct is an % array index. this.ParamList.(param_name) = ... feval([class(value),'.empty']); % Construct full subscript reference with respect to 'this' S = [struct('type', '.', 'subs', param_name), S]; % Assign the value of parameter this.ParamList = subsasgn(this.ParamList, S, value); end end function bool = isparam(this, param_name) bool = isfield(param_name, this.ParamList); end % Alias for addParam that is useful to ensure the correspondence % between metadata parameter names and object property names. function addObjProp(this, Obj, tag, varargin) addParam(this, tag, Obj.(tag), varargin{:}); end % Print metadata in a readable form function str = mdt2str(this) % Make the function spannable over arrays if isempty(this) str = ''; return elseif length(this) > 1 str_arr = arrayfun(@(x)mdt2str(x), this, ... 'UniformOutput', false); str = [str_arr{:}]; return end % Compose the list of parameter names expanded over subscripts % except for those which are already character arrays par_names = fieldnames(this.ParamList); % Expand parameters over subscripts, except for the arrays of % characters exp_par_names = cell(1, length(par_names)); max_nm_arr = zeros(1, length(par_names)); for i=1:length(par_names) % Expand parameter subscripts exp_par_names{i} = printSubs( ... this.ParamList.(par_names{i}), ... 'own_name', par_names{i}, ... 'expansion_test', @(y) ~ischar(y)); % Max name length for this parameter including subscripts max_nm_arr(i) = max(cellfun(@(x) length(x), ... exp_par_names{i})); end % Calculate width of the name column name_pad_length = min(max(max_nm_arr), this.pad_lim); % Compose a list of parameter values converted to char strings val_strs = cell(1, length(par_names)); % Width of the values column will be the maximum parameter % string width val_pad_length = 0; for i=1:length(par_names) tmp_nm = par_names{i}; % Iterate over parameter indices for j=1:length(exp_par_names{i}) tmp_exp_nm = exp_par_names{i}{j}; TmpS = str2substruct(tmp_exp_nm); % Get the indexed value of parameter TmpS = [struct('type','.','subs', tmp_nm), TmpS]; %#ok tmp_val = subsref(this.ParamList, TmpS); %Do the check to detect unsupported data type if ischar(tmp_val) && ~isvector(tmp_val) && ... ~isempty(tmp_val) warning(['Argument ''%s'' is a multi-dimensional ',... 'character array. It will be converted to ',... 'single string during saving. Use cell',... 'arrays to save data as a set of separate ',... 'strings.'], tmp_exp_nm) % Flatten tmp_val = tmp_val(:); end % Check for new line symbols in strings if (ischar(tmp_val) || isstring(tmp_val)) && ... any(ismember({newline, sprintf('\r')},tmp_val)) warning(['String values must not contain ' ... '''\\n'' and ''\\r'' symbols, replacing ' ... 'them with '' ''.']); tmp_val = replace(tmp_val, ... {newline, sprintf('\r')}, ' '); end fmt_spec = this.ParamOptList.(tmp_nm).fmt_spec; if isempty(fmt_spec) % Convert to string with format specifier % extracted from the varaible calss val_strs{i}{j} = var2str(tmp_val); else val_strs{i}{j} = sprintf(fmt_spec, tmp_val); end % Find maximum length to determine the colum width, % but, for beauty, do not account for variables with % excessively long value strings tmplen = length(val_strs{i}{j}); if (val_pad_length else str = [str, sprintf([par_fmt_spec, ls],... exp_par_names{i}{j}, val_strs{i}{j})]; %#ok end end end % Prints an extra line separator at the end str = [str, sprintf(ls)]; end % Save metadata to a file function save(this, filename) fileID = fopen(filename, 'a'); fprintf(fileID, mdt2str(this)); fclose(fileID); end - % Create a structure from metadata array - function MdtList = arrToStruct(this) - MdtList = struct(); - - for i = 1:length(this) - fn = matlab.lang.makeValidName(this(i).title); - MdtList.(fn) = this(i); - end + % Select objects with given titles from an array of metadata + function varargout = titleref(this, varargin) + ind = ismember({this.title}, varargin); + [varargout{1:nargout}] = this(ind); end end methods (Access = public, Static = true) % Create metadata indicating the present moment of time function TimeMdt = time(varargin) TimeMdt = MyMetadata(varargin{:}); if isempty(TimeMdt.title) TimeMdt.title = 'Time'; end dv = datevec(datetime('now')); addParam(TimeMdt, 'Year', dv(1), 'fmt_spec','%i'); addParam(TimeMdt, 'Month', dv(2), 'fmt_spec','%i'); addParam(TimeMdt, 'Day', dv(3), 'fmt_spec','%i'); addParam(TimeMdt, 'Hour', dv(4), 'fmt_spec','%i'); addParam(TimeMdt, 'Minute', dv(5), 'fmt_spec','%i'); addParam(TimeMdt, 'Second', floor(dv(6)), 'fmt_spec','%i'); addParam(TimeMdt, 'Millisecond',... round(1000*(dv(6)-floor(dv(6)))),'fmt_spec','%i'); end % Load metadata from file. Return all the entries found and % the number of the last line read. function [MdtArr, n_end_line] = load(filename, varargin) fileID = fopen(filename,'r'); MasterMdt = MyMetadata(varargin{:}); % Loop initialization MdtArr = MyMetadata.empty(); line_no = 0; % Loop continues until we reach the next header or we reach % the end of the file while ~feof(fileID) % Grabs the current line curr_line = fgetl(fileID); % Give a warning if the file is empty, i.e. if fgetl % returns -1 if curr_line == -1 disp(['Read empty file ', filename, '.']); break end % Skips if the current line is empty if isempty(deblank(curr_line)) continue end S = parseLine(MasterMdt, curr_line); switch S.type case 'title' % Add new a new metadata to the output list TmpMdt = MyMetadata(varargin{:}, 'title', S.match); MdtArr = [MdtArr, TmpMdt]; %#ok case 'paramval' % Add a new parameter-value pair to the current % metadata [name, value, comment, Subs] = S.match{:}; if ~isparam(TmpMdt, name) % Add new parameter addParam(TmpMdt, name, value, ... 'SubStruct', Subs, 'comment', comment); else % Assign the value to a new subscript of % an existing parameter Subs = [struct('type','.','subs',name), Subs]; %#ok this.ParamList = subsasgn(this.ParamList, ... Subs, value); end otherwise % Exit break end % Increment the counter line_no = line_no+1; end n_end_line = line_no; fclose(fileID); end end methods (Access = protected) % Parse string and determine the type of string function S = parseLine(this, str) S = struct( ... 'type', '', ... % title, paramval, other 'match', []); % parsed output % Check if the line contains a parameter - value pair. % First separate the comment if present pv_token = regexp(str, this.comment_sep, 'split', 'once'); if length(pv_token)>1 comment = pv_token{2}; % There is a comment else comment = ''; % There is no comment end % Then process name-value pair. Regard everything after % the first column separator as value. pv_token = regexp(pv_token{1}, this.column_sep, 'split', ... 'once'); % Remove leading and trailing blanks pv_token = strtrim(pv_token); if length(pv_token)>=2 % A candidate for parameter-value pair is found. Infer % the variable name and subscript reference. [Subs, name] = str2substruct(pv_token{1}); if isvarname(name) % Attempt converting the value to a number value = str2doubleHedged(pv_token{2}); S.type = 'paramval'; S.match = {name, value, comment, Subs}; return end end % Check if the line contains a title title_exp = [this.hdr_spec, '(\w.*)', this.hdr_spec]; title_token = regexp(str, title_exp, 'once', 'tokens'); if ~isempty(title_token) % Title expression found S.type = 'title'; S.match = title_token{1}; return end % No match found S.type = 'other'; S.match = {}; end % Make custom footer for command line display % (see matlab.mixin.CustomDisplay) function str = getFooter(this) if length(this) == 1 % For a single object display its properties str = ['Content:', newline, newline, ... replace(mdt2str(this), sprintf(this.line_sep), ... newline)]; elseif length(this) > 1 % For a non-empty array of objects display titles str = sprintf('\tTitles:\n\n'); for i = 1:length(this) str = [str, sprintf('%i\t%s\n', i, this(i).title)]; %#ok end str = [str, newline]; else % For an empty array display nothing str = ''; end end end end