classdef flightLogSignalMapping < handle
    %flightLogSignalMapping Map flight logs to signals
    %   flightLogSignalMapping provides mapping tools to analyze and
    %   visualize flight logs. To inspect UAV logs, first load your file
    %   using a file or log reader like mavlinktlog or ulogreader 
    %   or ardupilotreader.
    %   Use pre-configured signal mapping and plots for ULOG or TLOG 
    %   or BIN log files, or define your own signal mapping using the 
    %   mapSignal function.
    %   Update or add new plots with the updatePlot function. Then, call
    %   show with a structure of data to display the list of configured
    %   plots defined in the AvailablePlots property.
    %
    %   For ease of use, specific predefined signals and plots are
    %   provided. Use the info function to list the available signals and
    %   plots for you to use.
    %
    %   mapping = flightLogSignalMapping creates a flight log mapping with
    %   no preset signal mapping and a set of default visualization
    %   routines. Before you can visualize signals, map signals using the
    %   mapSignal function.
    %
    %   mapping = flightLogSignalMapping("tlog") creates a flight log
    %   mapping for imported MAVLink TLOG message tables.
    %
    %   mapping = flightLogSignalMapping("ulog") creates a flight log
    %   mapping for imported PX4 ULOG files.
    % 
    %   mapping = flightLogSignalMapping("bin") creates a flight log
    %   mapping for imported BIN LOG files.
    %
    %   flightLogSignalMapping properties:
    %       MappedSignals  - Names of all mapped signals
    %       AvailablePlots - Names of plots that are available
    %
    %   flightLogSignalMapping methods:
    %       mapSignal      - Add or update signal mapping
    %       updatePlot     - Add or update plot functions
    %       extract        - Extract signals as timetables
    %       checkSignal    - Check the mapped signal
    %       show           - Generate plots for visual log inspection
    %       info           - Get information about signal mapping and plots
    %       copy           - Creates a deep copy of the object
    %
    %   Example:
    %
    %       % Create an ulog mapping
    %       mapping = flightLogSignalMapping("ulog");
    %
    %       % Load ulog data
    %       data = ulogreader("flight.ulg");
    %
    %       %Analyze the data
    %       show(mapping, data);
    %
    %       % Extract signal
    %       rpy = extract(mapping, data, "AttitudeEuler");
    %
    %       % Create an tlog mapping
    %       mapping = flightLogSignalMapping("tlog");
    %
    %       % Load tlog data
    %       data = mavlinktlog("flight.tlog");
    %       msgs = readmsg(data, "SystemID", 1, "ComponentID", 1);
    %
    %       % Analyze the log in v1
    %       show(mapping, msgs);
    %
    %       % Extract signal
    %       gpsData = extract(mapping, msgs, "GPS");
    % 
    %       % Create an ardupilot(bin) log mapping
    %       mapping = flightLogSignalMapping("bin");
    %
    %       % Load bin data
    %       data = ardupilotreader("flight.bin");
    %
    %       % Analyze the data
    %       show(mapping, data);
    %
    %       % Extract signal
    %       rpy = extract(mapping, data, "AttitudeEuler");
    %
    %   See also mavlinktlog, ulogreader, ardupilotreader
    
    %   Copyright 2019-2023 The MathWorks, Inc.
    
    properties (Dependent, SetAccess = private)
        %MappedSignals Names of all mapped signals
        MappedSignals
        
        %AvailablePlots Names of plots that are available
        %   Names of plots that are available based on the mapped signals,
        %   specified as a string array. To add plots to this list, either 
        %   map signals for the predefined plots or call updatePlot. Access
        %   predefined plots using: info(mapping,"Plots").
        AvailablePlots
    end
    
    properties (Access = {?flightLogSignalMapping, ...
            ?uav.internal.tabcompletion.flightlogplotter.TabUtils, ...
            ?matlab.unittest.TestCase})
        %Analyzer internal implementation of the flight log analyzer
        Analyzer
    end
    
    methods
        function signalNames = get.MappedSignals(obj)
            %getter for MappedSignals
            signalNames = obj.Analyzer.MappedSignals;
        end
        
        function plotNames = get.AvailablePlots(obj)
            %getter for AvailablePlots
            plotNames = obj.Analyzer.AvailablePlots;
        end
    end
    
    methods
        function obj = flightLogSignalMapping(mode)
            %flightLogSignalMapping
            
            obj.Analyzer = uav.internal.log.Analyzer(uav.internal.log.Signals.Data);
            if nargin > 0
                mode = validatestring(mode, ["ulog", "tlog", "bin"], "flightLogSignalMapping", "mode");
                % create analyzer with pre-defined signal table
                switch mode
                    case "tlog"
                        uav.internal.log.configureTLogAnalyzer(obj.Analyzer);
                    case "ulog"
                        uav.internal.log.configureULogAnalyzer(obj.Analyzer);
                    case "bin"
                        uav.internal.log.configureArduPilotLogAnalyzer(obj.Analyzer);
                end
            end
            % setup pre-defined plots
            uav.internal.log.plotter.mav.plotConfiguration(obj.Analyzer);
        end
        
        function mapSignal(obj, signalName, timeAccess, valueAccess, varargin)
            %mapSignal Map UAV flight log signal
            %   mapSignal(MAPPING, SIGNALNAME, TIMEFUNC, VALUEFUNC) maps
            %   the signal of name SIGNALNAME to a pair of function handles
            %   TIMEFUNC and VALUEFUNC. SIGNALNAME is a string scalar and
            %   must match one of the pre-defined signal. TIMEFUNC is a
            %   function handle in form T=TIMEFUNC(DATA) that returns a Nx1
            %   vector of duration objects T that represents timestamps of
            %   the signal stored in imported flight data. VALUEFUNC is a
            %   function handle in form of V=VALUEFUNC(DATA) that returns a
            %   NxM matrix V that represents values of the signal stored in
            %   imported flight log data.
            %
            %   mapSignal(MAPPING, SIGNALNAME, TIMEFUNC, VALUEFUNC,
            %   VARNAMES) additionally let user map their own signals with
            %   signal name and the signal field names VARNAMES. VARNAMES
            %   is a Mx1 string array, where M is the number of columns of
            %   the return matrix from VALUEFUNC
            %
            %   mapSignal(MAPPING, SIGNALNAME, TIMEFUNC, VALUEFUNC,
            %   VARNAMES, UNITS) additionally let user specify the UNITS
            %   for each column signal returned by VALUEFUNC. UNITS is a
            %   Mx1 string array, where M is the number of columns of the
            %   return matrix from VALUEFUNC
            %
            %   Example:
            %       mapping = flightLogSignalMapping;
            %       % Map pre-defined signal "Gyro"
            %       mapSignal(mapping, "Gyro", @(x)x.Gyro.Time, @(x)x.Gyro.Value);
            %       % Map custom-defined signal "Odom"
            %       mapSignal(mapping, "Odom", @(x)x.Odom.Time, @(x)x.Odom.Value, ["x", "y"], ["m" "m"]);
            
            narginchk(4,6);
            
            % verify signal name
            signalName = convertCharsToStrings(signalName);
            validateattributes(signalName, "string", "scalartext", "mapSignal", "signalName");
            if isempty(regexp(signalName, "^[a-zA-Z]\w*$", "match"))
                error(message("uav:robotuav:flightlogsignalmapping:UnsupportedSignalNameFormat", signalName));
            end
            
            % verify accessors
            validateattributes(timeAccess, "function_handle", "scalar", "mapSignal", "timeAccess");
            validateattributes(valueAccess, "function_handle", "scalar", "mapSignal", "valueAccess");

            if nargin > 4
                % verify variable names
                variableNames = convertCharsToStrings(varargin{1});
                validateattributes(variableNames, "string", ["nonempty", "vector"], "mapSignal", "variableNames");
                
                % verify units
                if nargin < 6
                    units = repelem("N/A", numel(variableNames),1);
                else
                    units = convertCharsToStrings(varargin{2});
                    validateattributes(units, "string", {"vector", "numel", numel(variableNames)}, "mapSignal", "units");
                end
                varargin = {variableNames, units};
            end
            
            obj.Analyzer.mapSignal(signalName, timeAccess, valueAccess, varargin{:})
        end
        
        function updatePlot(obj, plotName, plotFunc, requirements)
            %updatePlot Update UAV flight log plot functions
            %   updatePlot(MAPPING, PLOTNAME, PLOTFUNC, REQUIREDSIGNALS)
            %   adds or updates visualization routines stored in the
            %   analyzer. PLOTNAME is a string scalar that indicates the
            %   type of plot.
            %
            %   PLOTFUNC can be a function handle in form of
            %   F=PLOTFUNC(SIG1, SIG2, ...) that returns a handle F to the
            %   figure created by the PLOTFUNC. The figure would contain
            %   visualization of the signals SIG1, SIG2,..., SIGN is a
            %   struct of two fields "Names" and "Values" for the Nth
            %   required signal category. The "Names" field contains the
            %   names for the mapped signals as string array. The "Values"
            %   field contains the values of the mapped signals as a cell
            %   array of timetables. REQUIREDSIGNALS is a string array
            %   listing the names of the signals required by the PLOTFUNC
            %   to generate the required plot.
            %
            %   Alternatively, PLOTFUNC can be a string selected from a
            %   pre-defined set of plot functionalities: ["stackedplot",
            %   "plotXY", "plotXYZ"]. REQUIREDSIGNALS is a string array,
            %   listing the names of the signal fields required by the plot
            %   template. Each signal field has format of "xx.yy", where
            %   "xx" is signal name, "yy" is field name.
            %
            %   Example:
            %       mapping = flightLogSignalMapping;
            %       % add a custom IMU plot using plotIMU function
            %       updatePlot(mapping, "IMU", @(acc, gyro, mag)plotIMU(acc, gyro, mag), ["ACC", "GYRO", "MAG"]);
            %       % add a templated XYZ plot with LocalNED coordinates
            %       updatePlot(mapping, "NED", "XYZ", ["LocalNED.X", "LocalNED.Y", "LocalNED.Z"]);
            
            [plotName, plotFunc, requirements] = convertCharsToStrings(plotName, plotFunc, requirements);
            validateattributes(plotName, "string", "scalartext", "updatePlot", "plotName");
            if isempty(regexp(plotName, "^[a-zA-Z]\w*$", "match"))
                error(message("uav:robotuav:flightlogsignalmapping:UnsupportedPlotNameFormat", plotName));
            end
            validateattributes(plotFunc, ["function_handle", "string"], "scalar", "updatePlot", "plotHandle");
            if isstring(plotFunc)
                plotFunc = validatestring(plotFunc, ["Timeseries", "XY", "XYZ"], "updatePlot", "plotHandle");
            end
            validateattributes(requirements, "string", ["nonempty", "vector"], "updatePlot", "requirements");
            obj.Analyzer.updatePlot(plotName, plotFunc, requirements);
        end
        
        function [summary, errorIndex] = checkSignal(obj,data,varargin)
            %checkSignal check mapped signals using flight log data
            %  [SUMMARY,ERRORINDEX] = checkSignal(MAPPING, DATA) checks
            %  mapped signals using the flight log data. MAPPING is the 
            %  flightLogSignalMapping object. DATA is flight log data 
            %  imported into MATLAB. DATA can be a table returned by
            %  either mavlinktlog, ulogreader object, or other custom
            %  formats.
            %
            %  [SUMMARY,ERRORINDEX] = checkSignal(MAPPING,DATA,NAME,VALUE) 
            %  specifies additional name, value parameters described below:
            %
            %  'Signal'- A string array that specifies the name of the 
            %  mapped signal to be read from input. By default all the 
            %  mapped signals are considered.
            %  
            %  'Preview'- Parameter that specifies if the preview of the 
            %  extracted signal must be displayed. Specify the value as 
            %  'On' to generate a preview. Otherwise, specify value as 
            %  'Off'. By default, the value is 'Off'.
            %
            %  The output SUMMARY is a struct array with fields SignalName 
            %  and Result. Result is a message string that specifies if the
            %  mapped signals are extracted successfully (pass message) or 
            %  unsuccessfully (error message).
            %
            %  The output ERRORINDEX has the index of unsuccessful signal
            %  extraction.
            %
            %   Example:
            %       % check all the mapped signals
            %       mapping = flightLogSignalMapping("ulog");
            %       logData = ulogreader("flight.ulg");
            %       [summary, errorIndex] = checkSignal(mapping, logData);
            %
            %       % check all the mapped signals interactively
            %       [summary, errorIndex] = checkSignal(mapping, logData,"Preview","on");
            %
            %       % check specific set of signals
            %       [summary, errorIndex] = checkSignal(mapping, logData,"Signal",["Accel" "Barometer"]);
            
            narginchk(2,6);
            
            %set the expected preview to 'on' and 'off'
            expectedPreview  = {'on', 'off'};
            
            p = inputParser;
            addParameter(p,'Preview','off',@(x)any(validatestring(x,expectedPreview,"checkSignal","Preview")));
            addParameter(p,'Signal',obj.MappedSignals,@(x)validateattributes(convertCharsToStrings(x),...
                "string", ["nonempty", "vector"],"checkSignal","Signal"));
            
            %Parse the input
            parse(p,varargin{:});
            
            signalNames = convertCharsToStrings(p.Results.Signal);
            preview = p.Results.Preview;
            
            if(strcmpi(preview,'on'))
                ps = 1;
            else
                ps = 0;
            end
            
            %verify the SignalNames and obtain the output.
           [summary, errorIndex]= obj.Analyzer.verifyMapping(data, signalNames,ps);
        end
        
        function signals = extract(obj, data, signalNames, ts, te)
            %EXTRACT Extract UAV flight log signals as timetables
            %   SIGNALS = EXTRACT(MAPPING, DATA, SIGNALNAMES) obtains
            %   signals that are named in SIGNALNAMES as timetables from
            %   DATA. DATA is flight log data imported into MATLAB. DATA
            %   could be a table returned by mavlinktlog.readmsg,
            %   ulogreader object, or other custom formats. SIGNALNAMES is
            %   a string array. SIGNALS is a cell array of timetables
            %
            %   SIGNALS = EXTRACT(MAPPING, DATA, SIGNALNAMES, TS) extracts
            %   signals with timestamp >= TS. TS is a scalar duration
            %   object.
            %
            %   SIGNALS = EXTRACT(MAPPING, DATA, SIGNALNAMES, TS, TE)
            %   extracts signals with timestamp within [TS, TE] interval.
            %   TS, TE are scalar duration objects.
            %
            %   Example:
            %
            %       mapping = flightLogSignalMapping("ulog");
            %       logData = ulogreader("flight.ulg");
            %       tbls = extract(mapping, logData);
            
            narginchk(2,5);
            
            if nargin < 3
                signalNames = obj.MappedSignals;
            else
                signalNames = convertCharsToStrings(signalNames);
                validateattributes(signalNames, "string", ["nonempty", "vector"], "extract", "signalNames");
            end
            
            if nargin < 4
                ts = seconds(-inf);
            else
                validateattributes(ts, "duration", ["scalar", "nonnan"], "extract", "ts");
            end
            
            if nargin < 5
                te = seconds(inf);
            else
                validateattributes(te, "duration", ["scalar", "nonnan"], "extract", "te");
            end
            
            signals = obj.Analyzer.extract(data, signalNames, ts, te);
        end
        
        function h = show(obj, data, varargin)
            %SHOW Display plots for inspection of UAV logs
            %  H = SHOW(MAPPING, DATA) generates all the plot stored in the
            %  flightLogSignalMapping, MAPPING. DATA is flight log data
            %  imported into MATLAB. DATA could be table returned from
            %  mavlinktlog.readmsg, ulogreader object, or other custom
            %  formats. A struct that pairs plot names and figure handles,
            %  H, is returned.
            %
            %  H = SHOW(MAPPING, DATA, TS) plots all data starting at the
            %  given start time, TS. TS is a duration scalar.
            %
            %  H = SHOW(MAPPING, DATA, TS, TE) plots all data within the
            %  time interval [TS TE] inclusive.
            %
            %  H = SHOW(__, NAME-VALUE) additionally takes following name
            %  value pairs:
            %        PlotsToShow       - A string array containing names of
            %                            plots to generate
            %
            %  Example:
            %
            %      mapping = flightLogSignalMapping("ulog");
            %      logData = ulogreader("flight.ulg");
            %      show(mapping, logData);
            
            parser = inputParser;
            parser.addOptional("TS", seconds(-inf), ...
                @(ts)validateattributes(ts, "duration", ["scalar", "nonnan"], "show", "ts"));
            parser.addOptional("TE", seconds(inf), ...
                @(te)validateattributes(te, "duration", ["scalar", "nonnan"], "show", "te"));
            parser.addParameter("PlotsToShow", obj.Analyzer.AllPlots, ...
                @(plotNames)validateattributes(convertCharsToStrings(plotNames), "string", ["nonempty", "vector"], "show", "plotNames"));
            parser.addParameter("SynchronizeParams", obj.Analyzer.SyncParam, @(x)verifySyncParam(x));
            
            parser.parse(varargin{:});
            ts = parser.Results.TS;
            te = parser.Results.TE;
            plotsToShow = convertCharsToStrings(parser.Results.PlotsToShow);
            obj.Analyzer.SyncParam = parser.Results.SynchronizeParams;
            
            % show plots
            handles = obj.Analyzer.show(data, ts, te, plotsToShow);
            if nargout == 1
                h = handles;
            end
            
            function valid = verifySyncParam(syncParam)
                validateattributes(syncParam, "cell", ["nonempty", "vector"], "show", "SynchronizeParams");
                try
                    synchronize(timetable(1, 'RowTimes', seconds(1)), timetable(1, 'RowTimes', seconds(1)), syncParam{:});
                    valid = true;
                catch ME
                    error(message("uav:robotuav:flightlogsignalmapping:InvalidSyncParam", ME.message));
                end
            end
        end
        
        function tbl = info(obj, type, names)
            %INFO Signal mapping and plot information for UAV log mapping
            %  TBL = INFO(MAPPING, "Signal") generates a table TBL that
            %  contains field names and field units for signals that are
            %  either predefined or mapped inside the MAPPING object.
            %  TBL also contains a column "IsMapped" that indicates whether
            %  a given signal is mapped with time and value function
            %  handles.
            %
            %  TBL = INFO(MAPPING, "Signal", SIGNALNAMES) generates the
            %  previous table, but only for signals that matched
            %  SIGNALNAMES. SIGNALNAMES is an array of strings.
            %
            %  TBL = INFO(MAPPING, "Plot") generates a table TBL that
            %  contains the required signals for all plotters stored in the
            %  MAPPING.
            %
            %  TBL = INFO(MAPPING, "Plot", PLOTNAMES) generates the
            %  previous table for the specified PLOTNAMES as an array of
            %  strings.
            %
            %  Example:
            %
            %      mapping = flightLogSignalMapping;
            %      info(mapping, "Signal")
            %      info(mapping, "Plot")
            
            narginchk(2, 3);
            type = validatestring(type, ["Signal", "Plot"], "info", "type");
            switch type
                case "Signal"
                    if nargin < 3
                        names = [uav.internal.log.Signals.Data.Name; obj.MappedSignals'];
                    else
                        names = convertCharsToStrings(names);
                        validateattributes(names, "string", ["nonempty", "vector"], "info", "names");
                    end
                    tbl = obj.Analyzer.signalInfo(names);
                case "Plot"
                    if nargin < 3
                        names = obj.Analyzer.AllPlots;
                    else
                        names = convertCharsToStrings(names);
                        validateattributes(names, "string", ["nonempty", "vector"], "info", "names");
                    end
                    tbl = obj.Analyzer.plotInfo(names);
            end
        end
        
        function c = copy(obj)
            %COPY Creates a deep copy of the object
            
            c = flightLogSignalMapping();
            c.Analyzer = copy(obj.Analyzer);
        end
    end
end

