diff --git a/@MyLog/MyLog.m b/@MyLog/MyLog.m index 1181e3c..2452ab6 100644 --- a/@MyLog/MyLog.m +++ b/@MyLog/MyLog.m @@ -1,677 +1,701 @@ % 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 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: %.2g' % Data columns are separated by this symbol data_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' 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 end properties (SetAccess=public, GetAccess=public) timestamps % Times at which data was aqcuired data % Array of measurements TimeLabels % Structure array that stores labeled time marks % Structure array that stores all the axes the log is plotted in PlotList; % Information about the log in saveable format, % including time labels and data headers Metadata 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) %% Constructor and destructor methods function this = MyLog(varargin) P=MyClassParser(this); processInputs(P, this, varargin{:}); this.Metadata=MyMetadata(P.unmatched_nv{:}); % Create an empty structure array of time labels this.TimeLabels=struct(... 'time',{},... % datetime object 'time_str',{},... % time in text format 'text_str',{}); % message string % Create an empty structure array of axes this.PlotList=struct(... 'Axes',{},... % axes handles 'DataLines',{},... % data line handles 'LbLines',{},... % labels line handles 'LbText',{}); % labels text handles % Load the data from file if the file name was provided if ~ismember('file_name', P.UsingDefaults) load(this, P.Results.file_name); end end %% Save and load functionality % save the entire data record function save(this, fname) % 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 fname = this.file_name; else this.file_name=fname; end assert(~isempty(fname), 'File name is not provided.'); datfname=this.data_file_name; metfname=this.meta_file_name; stat=createFile(datfname); if stat % Save time labels in separate file save(this.Metadata, metfname, 'overwrite', true); fid = fopen(datfname,'w'); - printDataHeaders(this, datfname); + % 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 end % Load log from file function load(this, fname) if nargin()==2 this.file_name=fname; end assert(~isempty(this.file_name), 'File name is not provided.'); assert(exist(fname, 'file')==2, ['File ''',fname,... ''' is not found.']) % Load metadata if file is found % Fields of Metadata are re-initialized by its get method, so % need to copy in order for the loaded information to be not % overwritten, on one hand, and on the other hand to use the % formatting defined by Metadata. M=copy(this.Metadata); clearFields(M); if exist(this.meta_file_name, 'file')==2 load(M, this.meta_file_name); end % Read column headers from data file fid=fopen(fname,'r'); dat_col_heads=strsplit(fgetl(fid),this.data_column_sep, ... 'CollapseDelimiters', true); fclose(fid); % Read data as delimiter-separated values and convert to cell % array, skip the first line containing column headers fulldata = dlmread(fname, this.column_sep, 1, 0); this.data = fulldata(:,2:end); this.timestamps = fulldata(:,1); % Process metadata % 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. if ismember('ColumnNames', M.field_names) && ... length(M.ColumnNames.Name.value)>=2 % Assign column headers from metadata if present this.data_headers=M.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', M.field_names) Lbl=M.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); this.TimeLabels(i).text_str=Lbl(i).text_str; end end % Convert the time stamps to datetime if the time column % format is posixtime if ~isempty(dat_col_heads) && ... contains(dat_col_heads{1},'posix','IgnoreCase',true) this.timestamps=datetime(this.timestamps, ... 'ConvertFrom','posixtime'); end 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'' ',... + 'does not match the number of data columns']); + 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); + catch + warning(['Logger cannot save data at time = ',... + datestr(datetime('now'))]); + % 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 - delete(this.PlotList(ind).LbLines); - this.PlotList(ind).LbLines=[]; - delete(this.PlotList(ind).LbText); - this.PlotList(ind).LbText=[]; + eraseTimeLabels(this, Ax); % Define marker lines to span over the entire plot yminmax=ylim(Ax); ymin=yminmax(1); ymax=yminmax(2); - Ax.ClippingStyle='rectangle'; 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, 0.95, T.text_str,... 'Units','data',... 'HorizontalAlignment','right',... 'VerticalAlignment','bottom',... 'FontWeight', 'bold',... 'Rotation',90,... 'BackgroundColor','white',... - 'Clipping','off',... + '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 - - %% Manipulations with log data - - % Append data points 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(iscolumn(time),'Time and array must be column'); - assert(ismatrix(val),'Value must be matrix.') - [nrows, ~]=size(val); - assert(length(time)==nrows,... - 'Lengths of the time and value arrays do not match'); - - % 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 to file - if p.Results.save - try - exstat = exist(this.file_name,'file'); - if exstat==0 - % if the file does not exist, create it and write - % the metadata - createFile(this.file_name); - printAllHeaders(this.Metadata, this.file_name); - fid = fopen(this.file_name,'a'); - else - % otherwise open for appending - fid = fopen(this.file_name,'a'); - end - % Print new values to the file - for i=1:length(time) - fprintf(fid, this.data_line_fmt, ... - posixtime(time(i)), val(i)); - end - fclose(fid); - catch - warning(['Logger cannot save data at time = ',... - datestr(time(1))]); - % Try closing fid in case it is still open - try - fclose(fid); - catch - 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 to the metadata + % Add label % Form with optional arguments: addTimeLabel(this, time, str) function addTimeLabel(this, varargin) p=inputParser(); addOptional(p, 'time', datetime('now'), ... @(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(datetime('now'))}); if isempty(answ)||isempty(answ{1}) return else % Conversion of the inputed value to datetime to % ensure proper format time=datetime(answ{2}); % 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'), ... @(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 % Conversion of the inputed value to datetime to % ensure proper format time=datetime(answ{2}); % 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(1,length(this.TimeLabels)); + 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(:)=[]; - this.data_headers(:)=[]; + + % 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]; end end end end methods (Access=private) %% 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 file - function printDataHeaders(this, fname) + % Print column names to a string + function str=printDataHeaders(this) cs=this.data_column_sep; - fid=fopen(fname, 'a'); - fprintf(['%s',cs], this.column_headers{:}); - fclose(fid); + 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 newer labels - % have larger index + % Re-order the elements of TimeLabels array so that labels + % corresponding to later times have larger index function sortTimeLabels(this) - % Convert to table and sort rows - tbl=struct2table(this.TimeLabels); - this.TimeLabels=table2struct(sortrows(tbl)); - % Conversion to tables instead of matrices simplifies the code - % but takes longer to execute. - % So switch to matrices in the future if time delay is an ussue + times=[this.TimeLabels.time]; + [~,ind]=sort(times); + this.TimeLabels=this.TimeLabels(ind); 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.data_column_sep; - nl=this.TimeLabels.line_sep; + nl=this.line_sep; if isempty(this.data) l=0; else - % Use end of the data array for better robustness when - % appending a measurement - l=length(this.data{end}); + [~,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 function Mdt=get.Metadata(this) Mdt=this.Metadata; % Clear Metadata but preserve formatting clearFields(Mdt); % Add column names addField(Mdt, 'ColumnNames'); addParam(Mdt, 'ColumnNames', 'Name', this.column_headers) % Add time labels (textual part of TimeLabels structure) addField(Mdt, 'TimeLabels'); - Lbl=struct('time_str', this.TimeLabels.time_str,... - 'text_str', this.TimeLabels.text_str); + Lbl=struct('time_str', {this.TimeLabels.time_str},... + 'text_str', {this.TimeLabels.text_str}); addParam(Mdt, 'TimeLabels', 'Lbl', Lbl) end end end diff --git a/@MyMetadata/MyMetadata.m b/@MyMetadata/MyMetadata.m index 77b68cc..1c2fd41 100644 --- a/@MyMetadata/MyMetadata.m +++ b/@MyMetadata/MyMetadata.m @@ -1,404 +1,404 @@ % MyMetadata stores parameter-value pairs grouped by fields. MyMetadata can % be saved and read in a readable text format. % 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 < dynamicprops & matlab.mixin.Copyable properties (Access=public) % Header sections are separated by [hdr_spec,hdr_spec,hdr_spec] hdr_spec='==' % Data starts from the line next to [hdr_spec,end_header,hdr_spec] end_header='Data' % 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=12 end properties (Access=private) PropHandles %Used to store the handles of the dynamic properties end properties (Dependent=true) field_names end methods function [this,varargout]=MyMetadata(varargin) P=MyClassParser(this); addParameter(P, 'load_path','',@ischar); processInputs(P,this, varargin{:}); load_path=P.Results.load_path; this.PropHandles=struct(); if ~isempty(load_path) varargout{1}=load(this, load_path); end end %Fields are added using this command. The field is a property of %the class, populated by the parameters with their values and %string specifications for later printing function addField(this, field_name) assert(isvarname(field_name),... 'Field name must be a valid MATLAB variable name.'); assert(~ismember(field_name, this.field_names),... ['Field with name ',field_name,' already exists.']); this.PropHandles.(field_name)=addprop(this,field_name); this.PropHandles.(field_name).SetAccess='protected'; this.PropHandles.(field_name).NonCopyable=false; this.(field_name)=struct(); end %Deletes a named field function deleteField(this, field_name) assert(isvarname(field_name),... 'Field name must be a valid MATLAB variable name.'); assert(ismember(field_name,this.field_names),... ['Attemped to delete field ''',field_name ... ,''' that does not exist.']); % Delete dynamic property from the class delete(this.PropHandles.(field_name)); % Erase entry in PropHandles this.PropHandles=rmfield(this.PropHandles,field_name); end %Clears the object of all fields - function clear(this) + function clearFields(this) cellfun(@(x) deleteField(this, x), this.field_names) end % Copy all the fields of another Metadata object to this object function addMetadata(this, Metadata) assert(isa(Metadata,'MyMetadata'),... 'Input must be of class MyMetadata, current input is %s',... class(Metadata)); assert(~any(ismember(this.field_names,Metadata.field_names)),... ['The metadata being added contain fields with the same ',... 'name. This conflict must be resolved before adding']) for i=1:length(Metadata.field_names) fn=Metadata.field_names{i}; addField(this,fn); param_names=fieldnames(Metadata.(fn)); cellfun(@(x) addParam(this,fn,x,Metadata.(fn).(x).value,... 'fmt_spec', Metadata.(fn).(x).fmt_spec,... 'comment', Metadata.(fn).(x).comment),... param_names); end end %Adds a parameter to a specified field. The field must be created %first. function addParam(this, field_name, param_name, value, varargin) assert(ischar(field_name),'Field name must be a char'); assert(isprop(this,field_name),... '%s is not a field, use addField to add it',param_name); assert(ischar(param_name),'Parameter name must be a char'); 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{:}); S=p.Results.SubStruct; comment=p.Results.comment; %Making sure that the comment does not %contain new line or carriage return characters, which would %mess up formating when saving metadata newline_smb={sprintf('\n'),sprintf('\r')}; %#ok if contains(comment, newline_smb) warning(['Comment string for ''%s'' must not contain ',... '''\\n'' and ''\\r'' symbols, replacing them ',... 'with space.'], param_name); comment=replace(comment, newline_smb,' '); end this.(field_name).(param_name).comment=comment; if isempty(S) % Assign value directly this.(field_name).(param_name).value=value; else % Assign using subref structure tmp=feval([class(value),'.empty']); this.(field_name).(param_name).value=subsasgn(tmp,S,value); end this.(field_name).(param_name).fmt_spec=p.Results.fmt_spec; end function save(this, filename, varargin) createFile(filename, varargin{:}); addTimeField(this); for i=1:length(this.field_names) printField(this, this.field_names{i}, filename); end printEndMarker(this, filename); end function printField(this, field_name, filename, varargin) %Takes optional inputs p=inputParser(); addParameter(p,'title',field_name); parse(p,varargin{:}); title_str=p.Results.title; ParStruct=this.(field_name); %Compose the list of parameter names expanded over subscripts %except for those which are already character arrays par_names=fieldnames(ParStruct); %Expand parameters over subscripts, except for the character %arrays exp_par_names=cell(1, length(par_names)); maxnmarr=zeros(1, length(par_names)); for i=1:length(par_names) tmpval=ParStruct.(par_names{i}).value; exp_par_names{i}=printSubs(tmpval, ... 'own_name', par_names{i}, ... 'expansion_test',@(y) ~ischar(y)); %Max name length for this parameter including subscripts maxnmarr(i)=max(cellfun(@(x) length(x), exp_par_names{i})); end %Calculate width of the name column name_pad_length=min(max(maxnmarr), this.pad_lim); %Compose list of parameter values converted to char strings par_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) TmpPar=ParStruct.(par_names{i}); for j=1:length(exp_par_names{i}) tmpnm=exp_par_names{i}{j}; TmpS=str2substruct(tmpnm); if isempty(TmpS) tmpval=TmpPar.value; else tmpval=subsref(TmpPar.value, TmpS); end %Do check to detect unsupported data type if ischar(tmpval)&&~isvector(tmpval)&&~isempty(tmpval) 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.'],tmpnm) % Flatten tmpval=tmpval(:); end %Check for new line symbols in strings if (ischar(tmpval)||isstring(tmpval)) && ... any(ismember({newline,sprintf('\r')},tmpval)) warning(['String value must not contain ',... '''\\n'' and ''\\r'' symbols, replacing them ',... 'with '' ''']); tmpval=replace(tmpval,{newline,sprintf('\r')},' '); end if isempty(TmpPar.fmt_spec) % Convert to string with format specifier % extracted from the varaible calss par_strs{i}{j}=var2str(tmpval); else par_strs{i}{j}=sprintf(TmpPar.fmt_spec, tmpval); end % Find maximum length to determine the colum width, % but, for beauty, do not account for variables with % excessively long value strings tmplen=length(par_strs{i}); if (val_pad_length1 % the line has comment comment_str=tmp{2}; else comment_str=''; end % Then process name-value pair. Regard everything after % the first column separator as value. tmp=regexp(tmp{1},this.column_sep,'split','once'); if length(tmp)<2 % Ignore the line if a name-value pair is not found continue else % Attempt convertion of value to number val=str2doubleHedged(strtrim(tmp{2})); end % Infer the variable name and subscript reference try [S, name]=str2substruct(strtrim(tmp{1})); catch name=''; end if isempty(name) % Ignore the line if variable name is not missing continue elseif ismember(name, fieldnames(this.(curr_title))) % If the variable name already presents among % parameters, add new subscript value this.(curr_title).(name).value= ... subsasgn(this.(curr_title).(name).value,S,val); else % Add new parameter with comment addParam(this, curr_title, name, val,... 'SubStruct', S, 'comment', comment_str); end end end fclose(fileID); if isempty(this.field_names) warning('No metadata found, continuing without metadata.') n_end_header=1; else n_end_header=line_no; end end end methods function field_names=get.field_names(this) field_names=fieldnames(this.PropHandles); end end end \ No newline at end of file diff --git a/GUIs/GuiLogger.mlapp b/GUIs/GuiLogger.mlapp index 8b6cf7f..57b0db3 100644 Binary files a/GUIs/GuiLogger.mlapp and b/GUIs/GuiLogger.mlapp differ