classdef ibeoMessageReader < handle & matlab.mixin.internal.Scalar & ...
        matlab.mixin.Copyable
    %ibeoMessageReader Object for reading message content from Ibeo IDC files.
    %   The ibeoMessageReader object is an indexed selection of the messages in
    %   an Ibeo IDC file. Use this object to read message content from an IDC
    %   file. Each object contains content only for messages of the type
    %   selected from an ibeoFileReader object. To create an ibeoMessageReader
    %   object, use the ibeoFileReader/select method.
    %
    %   ibeoMessageReader properties:
    %      FileName         - Absolute path to the IDC file (read-only)
    %      StartTime        - Timestamp of first message (read-only)
    %      EndTime          - Timestamp of last message (read-only)
    %      CurrentTime      - Timestamp of current message
    %      Timestamps       - Timestamps of all messages (read-only)
    %      Duration         - Total duration of the selection in seconds (read-only)
    %      MessageType      - Selected message type (read-only)
    %      NumMessages      - Number of messages in this selection (read-only)
    %      DeviceID         - IDs of the devices associated with this selection (read-only)
    %
    %   ibeoMessageReader methods:
    %      readMessages     - Read messages from selection
    %      readNextMessage  - Read next message from selection
    %      hasNextMessage   - Check if selection has next message
    %      reset            - Reset to first message in selection
    %
    %   Notes
    %   -----
    %   - CurrentTime affects the result of readNextMessage and hasNextMessage
    %     only and not that of readMessages.
    %   - If MessageType is 'object' and the IDC file contains both online
    %     objects and postprocessed objects, postprocessed objects are read
    %     along with online objects. Therefore, the StartTime, EndTime,
    %     CurrentTime, Timestamps, Duration, and NumMessages properties
    %     correspond to that of online object messages.
    %   - If MessageType is 'object' and the IDC file contains only postprocessed
    %     data, the Timestamps correspond to the postprocessing time, not the
    %     time of data collection. For synchronization purposes, use the
    %     'MidScanTimeStamp' field from the object metadata returned by
    %     ibeoMessageReader/readMessages or ibeoMessageReader/readNextMessage.
    %
    %   Example: Read all images in the first minute of the file
    %   --------------------------------------------------------
    %   % Create an Ibeo IDC file reader object.
    %   % Replace <idc_file_name> with your own Ibeo IDC file name.
    %   ibeoReader = ibeoFileReader(<idc_file_name>);
    %
    %   % Select all the images in the first minute.
    %   imgReader = select(ibeoReader, 'image', [0 minutes(1)]);
    %
    %   % Read all the messages in imgReader.
    %   imgs = readMessages(imgReader);
    %
    %   See also ibeoFileReader, ibeoFileReader/select,
    %            ibeoMessageReader/readMessages, ibeoMessageReader/readNextMessage.

    %   Copyright 2020-2021 The MathWorks, Inc.

    properties (GetAccess = 'public', SetAccess = 'private')
        % FileName Absolute path to the IDC file to be read, specified as a
        % string.
        FileName (1,1) string

        % StartTime Timestamp of first message in this selection represented
        % as datetime object with precision up to a nanosecond.
        StartTime (1,1) datetime

        % EndTime Timestamp of last message in this selection represented
        % as datetime object with precision up to a nanosecond.
        EndTime (1,1) datetime
    end

    properties (Dependent)
        % CurrentTime Timestamp of current message in this selection,
        % specified as a datetime scalar with precision up to a nanosecond.
        % CurrentTime ranges from StartTime and EndTime. Set CurrentTime to
        % a particular timestamp to read message from that timestamp using
        % readNextMessage.
        CurrentTime (1,1) datetime
    end

    properties (GetAccess = 'public', SetAccess = 'private')
        % Timestamps Timestamps of all messages in this selection,
        % specified as a NumMessages-by-1 datetime vector.
        Timestamps (:,1) datetime

        % Duration Total duration of the selection in seconds, specified as
        % a duration scalar with precision up to a second.
        Duration (1,1) duration

        % MessageType Selected message type in this selection, specified as
        % a string.
        MessageType (1,1) string

        % NumMessages Number of messages in this selection, specified as
        % a nonnegative integer.
        NumMessages (1,1) double

        % DeviceID IDs of the devices associated with this selection,
        % specified as integer vector.
        DeviceID (1,:) double
    end

    properties (Access = 'private')

        % CurrentTimestampIndex Local linear index of CurrentTimestamp.
        CurrentTimestampIndex (1,1) double
        
        % Version Version Number used for backward compatibility.
        Version = 1.0

        % DeviceIDs NumMessages-by-1 double vector of device IDs corresponding
        % to object's MessageType in the file.
        DeviceIDs (:,1) double        

        % MessagesPos NumMessages-by-1 uint64 vector of message positions
        % corresponding to object's MessageType in the file.
        MessagesPos (:,1) uint64                        
    end

    methods (Access = ?ibeoFileReader)
        %------------------------------------------------------------------
        % Constructor
        %------------------------------------------------------------------
        function this = ibeoMessageReader(selection)

            narginchk(1,2);

            this.FileName     = selection.FileName;
            this.MessageType  = selection.MessageType;
            this.Timestamps   = selection.Timestamps;
            this.StartTime    = selection.StartTime;
            this.EndTime      = selection.EndTime;
            this.DeviceID     = selection.DeviceID;
            this.DeviceIDs    = selection.DeviceIDs;
            this.MessagesPos  = selection.MessagesPos;
            this.Duration     = duration(this.EndTime - this.StartTime);
            this.NumMessages  = length(this.Timestamps);
            this.CurrentTimestampIndex = 1;            
        end

    end

    %----------------------------------------------------------------------
    % Public APIs
    %----------------------------------------------------------------------
    methods
        %------------------------------------------------------------------
        % ibeoMessageReader/readMessages
        %------------------------------------------------------------------
        function varargout = readMessages(this, varargin)
        %readMessages Read messages from Ibeo IDC file selection.
        %
        %   msgs = readMessages(msgReader) reads all the messages available
        %   in the ibeoMessageReader object, msgReader. msgs is an array of
        %   structures, pointClouds, or matrices depending on the MessageType
        %   of msgReader. For msgReader with 'object' as MessageType, msgs
        %   corresponds to online objects.
        %
        %   msgs = readMessages(msgReader, indices) reads the messages
        %   specified by the linear indices. The range of the linear
        %   indices argument is [1,msgReader.NumMessages].
        %
        %   msgs = readMessages(msgReader, timestamps) reads the messages
        %   specified by the timestamps. timestamps is an M-by-1 datetime or
        %   duration vector. The range of timestamps is
        %   [msgReader.StartTime, msgReader.EndTime] if it is a datetime
        %   vector or [0, msgReader.Duration] if it is a duration vector.
        %
        %   msgs = readMessages(msgReader, timeRange) reads the messages
        %   that are within a time range. timeRange is a datetime or a
        %   duration vector of the form [startTime endTime]. If timeRange
        %   is a duration vector, startTime and endTime are specified with
        %   respect to msgReader.StartTime.
        %
        %   [msgs, metadata] = readMessages(...) additionally returns a
        %   metadata structure array that has all the other fields present
        %   in the file that are not in msgs. msg and metadata are arrays
        %   of the same length. Refer to the <a href="matlab:helpview(fullfile(docroot,'toolbox','driving','helptargets.map'),'ibeoMessagesData')">documentation</a>
        %   to learn more about the content of msgs and metadata. When
        %   msgReader.MessageType is set to 'object', then this syntax does
        %   not apply.
        %
        %   [rawMsgs, procMsgs] = readMessages(...) returns online objects,
        %   rawMsgs, and postprocessed objects, procMsgs. This syntax
        %   applies only when msgReader.MessageType is 'object'.
        %
        %   [rawMsgs, procMsgs, rawMetadata, procMetadata] = readMessages(...)
        %   additionally returns the raw message metadata, rawMetadata, and
        %   postprocessed message metadata, procMetadata. This syntax
        %   applies only when msgReader.MessageType is 'object'. Refer to
        %   the <a href="matlab:helpview(fullfile(docroot,'toolbox','driving','helptargets.map'),'ibeoMessagesData')">documentation</a> to learn more about the
        %   content of the outputs.
        %
        %   [...] = readMessages(..., 'DeviceID', deviceID) specifies an
        %   additional name-value pair as described below:
        %
        %   'DeviceID'  Device IDs from msgReader.DeviceID to read messages
        %               from specific devices. This name-value pair cannot
        %               be used along with indices or timestamps as the
        %               second argument.
        %
        %               Default: msgReader.DeviceID
        %
        %   Notes
        %   -----
        %   - When working with large numbers of messages, this method can
        %     consume a lot of time and system memory. Consider using smaller
        %     batches of messages or using the readNextMessage method to
        %     read messages one by one.
        %
        %   Example 1: Read the first 10 images in the file.
        %   ------------------------------------------------
        %   % Create an Ibeo IDC file reader object.
        %   % Replace <idc_file_name> with your own Ibeo IDC file name.
        %   ibeoReader = ibeoFileReader(<idc_file_name>);
        %
        %   % Select images to be read.
        %   imgReader = select(ibeoReader, 'image');
        %
        %   % Read the first 10 images.
        %   imgs = readMessages(imgReader, 1:10);
        %
        %   Example 2: Read all object detections in the first 2 minutes.
        %   -------------------------------------------------------------
        %   % Create an Ibeo IDC file reader object.
        %   % Replace <idc_file_name> with your own Ibeo IDC file name.
        %   ibeoReader = ibeoFileReader(<idc_file_name>);
        %
        %   % Select images to be read.
        %   objReader = select(ibeoReader, 'object');
        %
        %   % Read all object detections in the first 2 minutes.
        %   [rawObjs, procObjs] = readMessages(objReader, minutes([0 2]));
        %
        %   Example 3: Read images in the first 2 minutes from single device.
        %   -----------------------------------------------------------------
        %   % Create an Ibeo IDC file reader object.
        %   % Replace <idc_file_name> with your own Ibeo IDC file name.
        %   ibeoReader = ibeoFileReader(<idc_file_name>);
        %
        %   % Select images to be read.
        %   imgReader = select(ibeoReader, 'image');
        %
        %   % Read the images.
        %   imgs = readMessages(imgReader, minutes([0 2]), ...
        %                       'DeviceID', imgReader.DeviceID(1));
        %
        %   See also ibeoFileReader, ibeoFileReader/select,
        %            ibeoMessageReader/readNextMessage.

            narginchk(1,4);
            
            import driving.internal.IbeoFileReaderImpl
            
            % Validate inputs and find valid file pointer indices.
            [msgIndices, syntaxID, deviceID] = validateAndParseInputs(this, varargin{:});
            msgPositions = findMessagePositions(this, msgIndices, syntaxID, deviceID);

            if ~isempty(msgPositions)
                % Read the messages based on the selection.
                messageID = uint16(hex2dec(IbeoFileReaderImpl.MessageTypesToIDsMap(this.MessageType)));
                rawData = IbeoFileReaderImpl.objectManager(this.FileName, "read", ...
                                                       messageID, msgPositions);

                % Package the output.
                unpackOutput = false;
                if this.MessageType ~= "object"
                    returnMetaData = nargout > 1;
                    [varargout{1}, varargout{2}] ...
                        = processRawData(this, rawData, returnMetaData, unpackOutput);
                else
                    returnMetaData = nargout > 2;
                    [varargout{1}, varargout{2}, varargout{3}, varargout{4}] ...
                        = processRawData(this, rawData, returnMetaData, unpackOutput);
                end
            else
                varargout = cell(1,2);
            end
        end

        %------------------------------------------------------------------
        % ibeoMessageReader/readNextMessage
        %------------------------------------------------------------------
        function varargout = readNextMessage(this, options)
        %readNextMessage Read next message from Ibeo IDC file selection.
        %
        %   msg = readNextMessage(msgReader) reads the next available
        %   message in the selection. msg is a structure, pointCloud, or matrix
        %   depending on the MessageType of msgReader. For msgReader with
        %   'object' as MessageType, msg corresponds to online objects. The
        %   msgReader object keeps track of the last read message for
        %   future calls to readNextMessage.
        %
        %   [msg, metadata] = readNextMessage(...) additionally returns a
        %   metadata structure that has all the other fields present in
        %   the file that are not in msg. Refer to the <a href="matlab:helpview(fullfile(docroot,'toolbox','driving','helptargets.map'),'ibeoMessageData')">documentation</a> to
        %   learn more about the content of msg and metadata. When
        %   msgReader.MessageType is set to 'object', then this syntax
        %   does not apply.
        %
        %   [rawMsg, procMsg] = readNextMessage(...) returns online
        %   objects, rawMsg, and postprocessed objects, procMsg. This
        %   syntax applies only when msgReader.MessageType is 'object'.
        %
        %   [rawMsg, procMsg, rawMetadata, procMetadata] = readNextMessage(...)
        %   additionally returns the raw message metadata, rawMetadata, and
        %   postprocessed message metadata, procMetadata. This syntax applies
        %   only when msgReader.MessageType is 'object'. Refer to the
        %   <a href="matlab:helpview(fullfile(docroot,'toolbox','driving','helptargets.map'),'ibeoMessageData')">documentation</a> to learn more about the content of the outputs.
        %
        %   [...] = readNextMessage(..., 'DeviceID', deviceID) specifies an
        %   additional name-value pair as described below:
        %
        %   'DeviceID'  Device IDs from msgReader.DeviceID to read messages
        %               from specific devices.
        %
        %               Default: msgReader.DeviceID
        %
        %   Example: Visualize video data from an IDC file.
        %   -----------------------------------------------
        %   % Replace <idc_file_name> with your own Ibeo IDC file name.
        %   ibeoReader = ibeoFileReader(<idc_file_name>);
        %
        %   timeRange = [0, minutes(1)];
        %   imgReader = select(ibeoReader, 'image', timeRange);
        %
        %   videoPlayer = vision.VideoPlayer;
        %
        %   % Loop through all the images in the reader from first device.
        %   while(hasNextMessage(imgReader))
        %     img = readNextMessage(imgReader);
        %     step(videoPlayer, img);
        %   end
        %
        %   release(videoPlayer);
        %
        %   See also ibeoFileReader, ibeoFileReader/select, ibeoMessageReader/reset
        %            ibeoMessageReader/hasNextMessage, ibeoMessageReader/readMessages.
            arguments
                this
                options.DeviceID = this.DeviceID
            end
            import driving.internal.IbeoFileReaderImpl
            
            funcName = "ibeoMessageReader/readNextMessage";
            deviceID = IbeoFileReaderImpl.validateDeviceID(options.DeviceID,...
                                                       this.DeviceID, this.MessageType, funcName);
            isMessageAvailable = false;

            % Find the index of a valid next message.
            nextIndex = this.CurrentTimestampIndex;
            while nextIndex <= this.NumMessages
                nextMessageDeviceID = this.DeviceIDs(nextIndex);
                if any(nextMessageDeviceID == deviceID)
                    isMessageAvailable = true;
                    break;
                end
                nextIndex = nextIndex + 1;
            end

            % Error out if there is no valid next message available.
            if isMessageAvailable
                this.CurrentTimestampIndex = nextIndex + 1;
            else
                error(message('driving:ibeoReader:endOfFile'));
            end

            % Read next message based on the selection.
            messageID = uint16(hex2dec(IbeoFileReaderImpl.MessageTypesToIDsMap(this.MessageType)));
            msgPosition = this.MessagesPos(nextIndex);
            rawData = IbeoFileReaderImpl.objectManager(this.FileName, "read", ...
                                                       messageID, msgPosition);
            
           % Package the output.
            unpackOutput = true;
            if this.MessageType ~= "object"
                returnMetaData = nargout > 1;
                [varargout{1}, varargout{2}] ...
                    = processRawData(this, rawData, returnMetaData, unpackOutput);
            else
                returnMetaData = nargout > 2;
                [varargout{1}, varargout{2}, varargout{3}, varargout{4}] ...
                    = processRawData(this, rawData, returnMetaData, unpackOutput);
            end
        end

        %------------------------------------------------------------------
        % ibeoMessageReader/hasNextMessage
        %------------------------------------------------------------------
        function flag = hasNextMessage(this)
        %hasNextMessage Check if Ibeo IDC file selection has next message.
        %
        %   flag = hasNextMessage(msgReader) returns true if there is a
        %   next message available to read from the ibeoMessageReader
        %   object, msgReader. Otherwise, it returns false.
        %
        %   Example: Visualize video data from an IDC file.
        %   -----------------------------------------------
        %   % Replace <idc_file_name> with your own Ibeo IDC file name.
        %   ibeoReader = ibeoFileReader(<idc_file_name>);
        %
        %   timeRange = [0, minutes(1)];
        %   imgReader = select(ibeoReader, 'image', timeRange);
        %
        %   videoPlayer = vision.VideoPlayer;
        %
        %   % Loop through all the images in the reader from first device.
        %   while(hasNextMessage(imgReader))
        %     img = readNextMessage(imgReader);
        %     step(videoPlayer, img);
        %   end
        %
        %   release(videoPlayer);
        %
        %   See also ibeoFileReader, ibeoFileReader/select, ibeoMessageReader/reset
        %            ibeoMessageReader/readNextMessage, ibeoMessageReader/readMessages.

            flag = this.CurrentTimestampIndex <= this.NumMessages;
        end

        %------------------------------------------------------------------
        % ibeoMessageReader/reset
        %------------------------------------------------------------------
        function reset(this)
        %reset Reset to first message in Ibeo IDC file selection.
        %
        %   reset(msgReader) resets the status of the msgReader object
        %   to the first message in the selection.
        %
        %   Example: Reset the ibeoMessageReader object.
        %   --------------------------------------------
        %   % Replace <idc_file_name> with your own Ibeo IDC file name.
        %   ibeoReader = ibeoFileReader(<idc_file_name>);
        %
        %   timeRange = [0, minutes(1)];
        %   imgReader = select(ibeoReader, 'image', timeRange);
        %
        %   videoPlayer = vision.VideoPlayer;
        %
        %   % Loop through all the images in the reader from first device.
        %   while(hasNextMessage(imgReader))
        %     img = readNextMessage(imgReader);
        %     step(videoPlayer, img);
        %   end
        %
        %   release(videoPlayer);
        %
        %   % View first image in the file selection in new figure.
        %   reset(imgReader);
        %   img = readNextMessage(imgReader);
        %   figure
        %   imshow(img);
        %
        %   See also ibeoFileReader, ibeoFileReader/select, ibeoMessageReader/hasNextMessage,
        %            ibeoMessageReader/readNextMessage, ibeoMessageReader/readMessages.

            this.CurrentTimestampIndex = 1;
        end
    end

    %----------------------------------------------------------------------
    % Setter and getter for dependent property CurrentTime.
    %----------------------------------------------------------------------
    methods
        %------------------------------------------------------------------
        function value = get.CurrentTime(this)
            if(this.CurrentTimestampIndex <= this.NumMessages)
                value = this.Timestamps(this.CurrentTimestampIndex);
            else
                value = this.EndTime;
            end
        end

        %------------------------------------------------------------------
        function set.CurrentTime(this, value)
            if(value < this.StartTime || value > this.EndTime)
                error(message('driving:ibeoReader:invalidIndexRange', 'time', ...
                              string(this.StartTime), string(this.EndTime)));
            end
            this.CurrentTimestampIndex = this.findTimeIndex(value);
        end
    end

    %----------------------------------------------------------------------
    % helper functions to process and package different message datatypes.
    %----------------------------------------------------------------------
    methods (Access = 'private')
        
        %------------------------------------------------------------------
        function varargout = processRawData(this, rawData, returnMetaData, unpackOutput)
            
            switch this.MessageType
                case "scan"
                    [varargout{1},varargout{2}]  = this.processScanData(rawData, returnMetaData);
                case "pointCloudPlane"
                    [varargout{1},varargout{2}]  = this.processPointCloudPlaneData(rawData, returnMetaData);
                case "object"
                    [varargout{1},varargout{2}, varargout{3}, varargout{4}] ...
                        = this.processObjectData(rawData, returnMetaData, unpackOutput);
                case "CAN"
                    [varargout{1},varargout{2}] = this.processCANData(rawData, returnMetaData);
                case "measurementList"
                    [varargout{1},varargout{2}] = this.processMeasurementListData(rawData, returnMetaData);
                case "vehicleState"
                    [varargout{1},varargout{2}] = this.processVehicleStateData(rawData, returnMetaData);
                case "image"
                    [varargout{1},varargout{2}] = this.processImageData(rawData, returnMetaData, unpackOutput);
                otherwise
                    assert(false, sprintf("Unsupported message type: %s.", this.MessageType));
            end
        end
        
        %------------------------------------------------------------------
        % Process and package data in scan messsages.
        %------------------------------------------------------------------
        function [ptCloud, metadata] = processScanData(~, rawData, returnMetaData)

            import driving.internal.IbeoFileReaderImpl
            
            ptCloud = pointCloud.empty(length(rawData),0);
            for i =  1:length(rawData)
                ptCloud(i) = pointCloud(rawData(i).PointsXYZ);                
            end
            
            if(~returnMetaData)
                metadata = [];
            else
                metadata = rmfield(rawData,'PointsXYZ');
                for i =  1:length(rawData)                
                    metadata(i).Timestamp = IbeoFileReaderImpl.convertNTPTime(rawData(i).Timestamp);
                    metadata(i).ScanStartTime = IbeoFileReaderImpl.convertNTPTime(rawData(i).ScanStartTime);
                    metadata(i).ScanEndTimeOffset = milliseconds(rawData(i).ScanEndTimeOffset/1000);
                end
            end
        end

        %------------------------------------------------------------------
        % Process and package data in pointCloudPlane messsages.
        %------------------------------------------------------------------
        function [ptCloud, metadata] = processPointCloudPlaneData(~, rawData, returnMetaData)

            ptCloud = pointCloud.empty(length(rawData),0);
            for i =  1:length(rawData)
                ptCloud(i) = pointCloud(rawData(i).PointsXYZ);
                if(~isempty(rawData(i).RGB))
                    ptCloud(i).Color = rawData(i).RGB;
                end
            end
            
            if(~returnMetaData)
                metadata = [];
            else
                metadata = rmfield(rawData,'PointsXYZ');
                for i =  1:length(rawData)
                    metadata(i).Timestamp = driving.internal.IbeoFileReaderImpl.convertNTPTime(rawData(i).Timestamp);
                    [label, referencePoint, planeOrientation] = processPointCloudPlaneMetaData(rawData(i));
                    metadata(i).Label = label;
                    metadata(i).ReferencePoint = referencePoint;
                    metadata(i).PlaneOrientation = planeOrientation;
                end
            end

            %--------------------------------------------------------------
            function [label, referencePoint, planeOrientation] = processPointCloudPlaneMetaData(rawData)
                pointsClassStrings = ["Undefined", "ScanPoint", "LanePoint", ...
                                    "CurbstonePoint" , "GuardrailPoint", "RoadmarkingPoint", ...
                                    "OffRoadMarkingPoint"];
                label = pointsClassStrings(1 + rawData.PointsKind);
                referencePoint = [rad2deg(rawData.RefPt_Lat),...
                                  rad2deg(rawData.RefPt_Long),...
                                  rawData.RefPt_Alt];
                planeOrientation = [rad2deg(rawData.PlaneOrientationYaw),...
                                    rad2deg(rawData.PlaneOrientationPitch),...
                                    rad2deg(rawData.PlaneOrientationRoll)];
            end
        end

        %------------------------------------------------------------------
        % Process and package data in object messsages.
        %------------------------------------------------------------------
        function [rawMsgs, procMsgs, rawMetadata, procMetadata] = processObjectData(~, rawData, returnMetaData, unpackOutput)                        
            
            if(~isempty(rawData.rawObj))
                [rawMsgs, rawMetadata] = iProcessObj(rawData.rawObj, returnMetaData);
                if unpackOutput
                    rawMsgs = rawMsgs{1};
                end
            else
                rawMsgs = [];
                rawMetadata = [];
            end

            if(~isempty(rawData.ppObj))
                [procMsgs, procMetadata] = iProcessObj(rawData.ppObj, returnMetaData);
                if unpackOutput
                    procMsgs = procMsgs{1};
                end
            else
                procMsgs = [];
                procMetadata = [];
            end

            %--------------------------------------------------------------
            function [output, metadata] = iProcessObj(rawData, returnMetaData)

                import driving.internal.IbeoFileReaderImpl
                
                classID = [0:6, 12,15,17];
                classNames = ["unclassified", "unknown small", "unknown big", ...
                              "pedestrian", "bike", "car", "truck", "underdriveable", ...
                              "motorbike","bicycle"];
                idToNamesMap = containers.Map(classID, classNames);
                obj = rmfield(rawData, {'MessageID','DeviceID', 'Timestamp', ...
                                    'MidScanTimeStamp', 'Flags', 'NumberOfObjects'});
                output = cell(length(rawData),1);
                for i =  1:length(rawData)
                    for j = 1 : rawData(i).NumberOfObjects

                        obj(i).ObjectList(j).ObjectTimeStamp = ...
                            IbeoFileReaderImpl.convertNTPTime(obj(i).ObjectList(j).ObjectTimeStamp);

                        obj(i).ObjectList(j).DerivedBoundingBoxCenterPointLocation = ...
                            iComputeBoundingBoxCenterPointLocation(...
                                obj(i).ObjectList(j).ReferencePointCoordinate(1), ...
                                obj(i).ObjectList(j).ReferencePointCoordinate(2), ...
                                obj(i).ObjectList(j).ObjectCourseAngle, ...
                                obj(i).ObjectList(j).ReferencePointLocation, ...
                                obj(i).ObjectList(j).ObjectBoxSize(1), ...
                                obj(i).ObjectList(j).ObjectBoxSize(2));
                        Classification =  idToNamesMap(obj(i).ObjectList(j).Classification);
                        obj(i).ObjectList(j).Classification = string(Classification);
                        types = 0:12;
                        datatypes = {'void', 'float', 'double', 'int8', 'uint8', ...
                                     'int16', 'uint16', 'int32', 'uint32', 'int64', ...
                                     'uint64',  'boolean', 'string'};
                        datatypeMap = containers.Map(types, datatypes);
                        for k = 1:obj(i).ObjectList(j).PropertyListLength
                            obj(i).ObjectList(j).Property(k).Type = datatypeMap(obj(i).ObjectList(j).Property(k).Type);
                        end
                    end
                    output{i} = obj(i).ObjectList;
                end
                if(returnMetaData)
                    metadata = rmfield(rawData,'ObjectList');
                    for i =  1:length(rawData)
                        metadata(i).Timestamp = IbeoFileReaderImpl.convertNTPTime(metadata(i).Timestamp);
                        metadata(i).MidScanTimeStamp = IbeoFileReaderImpl.convertNTPTime(metadata(i).MidScanTimeStamp);
                    end
                else
                    metadata = [];
                end

            end
        end
        
        %------------------------------------------------------------------
        % Process and package data in vehicleState messsages.
        %------------------------------------------------------------------
        function [output, metadata] = processVehicleStateData(~, rawData, returnMetaData)

            import driving.internal.IbeoFileReaderImpl
            
            metaFieldNames = {'Source','VehicleWidth', 'DistanceVehicleFrontToFrontAxle', ...
                              'DistanceRearAxleToFrontAxle', 'DistanceRearAxleToVehicleRear',...
                              'SteerRatioPoly0', 'SteerRatioPoly1', 'SteerRatioPoly2', 'SteerRatioPoly3',...
                              'DeviceID', 'NTPTimeStamp'};
            
            output = rmfield(rawData, metaFieldNames);
            outputFieldNames = fieldnames(output);
            for i =  1 : length(rawData)
                output(i).TimeStamp = IbeoFileReaderImpl.convertNTPTime(rawData(i).Timestamp);                
            end
            
            if(~returnMetaData)
                metadata = [];
            else
                metadata = rmfield(rawData, outputFieldNames);
                for i =  1 : length(rawData)
                    metadata(i).NTPTimeStamp = IbeoFileReaderImpl.convertNTPTime(rawData(i).NTPTimeStamp);                
                end
            end
            
        end
        
        %------------------------------------------------------------------
        % Process and package data in image messsages.
        %------------------------------------------------------------------
        function [output, metadata] = processImageData(~, rawData, returnMetaData, unpackOutput)

            import driving.internal.IbeoFileReaderImpl
            
            output = cell(length(rawData),1);            
            for i =  1 : length(rawData)
                output{i} = rawData(i).Image;
            end
            
            if unpackOutput
                output = output{1};
            end
            
            if(~returnMetaData)
                metadata = [];
            else
                metadata = rmfield(rawData, 'Image');
                for i =  1 : length(rawData)
                    metadata(i).Timestamp = IbeoFileReaderImpl.convertNTPTime(rawData(i).Timestamp);
                    metadata(i).NTPTimeStamp = IbeoFileReaderImpl.convertNTPTime(rawData(i).NTPTimeStamp);
                end
            end
        end
        
        %------------------------------------------------------------------
        % Process and package data in measurementList messsages.
        %------------------------------------------------------------------
        function [output, metadata] = processMeasurementListData(~, rawData, returnMetaData)

            import driving.internal.IbeoFileReaderImpl
            
            output = cell(length(rawData),1);
            for i =  1 : length(rawData)
                output{i} = iProcessMeasurements(rawData(i).Measurements);                
            end 
            
            if(~returnMetaData)
                metadata = [];
            else
                metadata = rmfield(rawData, 'Measurements');
                for i =  1 : length(rawData)
                    metadata(i).Timestamp = IbeoFileReaderImpl.convertNTPTime(rawData(i).Timestamp);
                    metadata(i).NTPTimeStamp = IbeoFileReaderImpl.convertNTPTime(rawData(i).NTPTimeStamp);
                    metadata(i).ElapsedMicrosecondsSincePowerOn = uint32(rawData(i).ElapsedMicrosecondsSincePowerOn);
                    metadata(i).ListName = [rawData(i).ListName, char(0)];
                    metadata(i).GroupName = [rawData(i).GroupName, char(0)];                    
                end 
            end
            
            %--------------------------------------------------------------
            function out = iProcessMeasurements(rawMeasurements)
                out = [];
                types = 0:12;
                datatypes = {'void', 'float', 'double', 'int8', 'uint8', ...
                             'int16', 'uint16', 'int32', 'uint32', 'int64', ...
                             'uint64',  'boolean', 'string'};
                datatypeMap = containers.Map(types, datatypes);
                for j = 1:length(rawMeasurements)
                    [temp.Key, temp.Unit] = iDecodeMeasurementKeyID(rawMeasurements(j).Key);
                    temp.Type = datatypeMap(rawMeasurements(j).TypeID);
                    temp.Data = rawMeasurements(j).Data;
                    out = [out, temp];
                end
            end
        end
        
        %------------------------------------------------------------------
        % Process and package data in CAN messsages.
        %------------------------------------------------------------------
        function [output, metadata] = processCANData(~,rawData, returnMetaData)

            import driving.internal.IbeoFileReaderImpl
            
            output = rmfield(rawData, {'Version', 'MessageType', 'DeviceTime_mu_s', ...
                                       'ReceivingTime', 'DeviceID', 'Timestamp', ...
                                       'CANDeviceID','MessageID'});
            
           if(~returnMetaData)
                metadata = [];
            else
                metadata = rmfield(rawData, {'CANMessageLength', 'Data', 'CAN_ID'});
                for i =  1 : length(rawData)
                    metadata(i).Timestamp = IbeoFileReaderImpl.convertNTPTime(rawData(i).Timestamp);
                    metadata(i).ReceivingTime = IbeoFileReaderImpl.convertNTPTime(rawData(i).ReceivingTime);
                end
           end           
        end                    
    end

    methods (Access = 'private')
        %------------------------------------------------------------------
        % helper function to convert timestamps to linear indices.
        %------------------------------------------------------------------
        function idx = findTimeIndex(this, value)

            [~,idx] = min(abs(this.Timestamps - value));            
        end

        %------------------------------------------------------------------
        % helper function for readMessages to find valid message positions
        % in the file.
        %------------------------------------------------------------------
        function msgPositions = findMessagePositions(this, msgIndices, argName, options)
        % Find valid message positions for different syntaxes.

            switch argName
                case "timestamps"
                    % Syntax 1: readMessages(msgReader, timestamps).
                    localIndices = zeros(size(msgIndices));
                    for i = 1:length(localIndices)
                        localIndices(i) = this.findTimeIndex(msgIndices(i));
                    end
                case "time range"
                    % Syntax 2: readMessages(msgReader, timeRange).                
                    localIndices = this.Timestamps >= msgIndices(1) & ...
                                   this.Timestamps <= msgIndices(2);
                case "index"
                    % Syntax 3: readMessages(msgReader, indices).
                    localIndices = msgIndices;
                otherwise
                    assert(false, sprintf("Invalid syntax, %s, for readMessages.", argName));
            end

            if(isfield(options, "DeviceID"))
                localIndices = ismember(this.DeviceIDs(localIndices), options.DeviceID);
            end
            
            msgPositions = this.MessagesPos(localIndices);
        end

        %------------------------------------------------------------------
        % input validator for readMessages
        %------------------------------------------------------------------
        function [msgIndices, syntaxID, deviceID] = validateAndParseInputs(this, msgIndices, options)
            arguments
                this
                msgIndices = [this.StartTime, this.EndTime]
                options.DeviceID
            end
            funcName = 'ibeoMessageReader/readMessages';
            [msgIndices, syntaxID] = validateMsgIndices(this, msgIndices, funcName);
            
            if isfield(options, 'DeviceID')
                if syntaxID ~= "time range"
                    error(message('driving:ibeoReader:invalidNVPair'));
                end
            else
                options.DeviceID = this.DeviceID;
            end
            
            deviceID = driving.internal.IbeoFileReaderImpl.validateDeviceID(...
                       options.DeviceID, this.DeviceID, this.MessageType, funcName);
                        
        end

        %------------------------------------------------------------------
        function [indices, argName] = validateMsgIndices(this, indices, funcName)
                        
            argName  = 'index';
            validateattributes(indices, ["duration","datetime","double"], ...
                    {'nonempty', 'vector'}, funcName, argName);                        
            
            % Convert duration indices to datetime indices.
            if isa(indices,"duration")
                indices = indices + this.StartTime;
            end            
            
            validRange = [this.StartTime this.EndTime];
            if isa(indices, "datetime") && iscolumn(indices)
                % Syntax 1: readMessages(msgReader, timestamps).
                argName = "timestamps";                
                invalidRange = any(indices < validRange(1)) || ...
                               any(indices > validRange(2)) || ...
                               any(isnat(indices));
            elseif isa(indices, "datetime") && isrow(indices)
                % Syntax 2: readMessages(msgReader, timeRange).
                argName = "time range";
                validateattributes(indices, "datetime", {'ncols', 2}, ...
                                   funcName, argName);
                invalidRange = indices(1) < validRange(1) || ...
                               indices(2) > validRange(2) || ...
                               indices(1) > indices(2)    || ...
                               any(isnat(indices));
            else
                % Syntax 3: readMessages(msgReader, indices).
                argName = "index";
                validateattributes(indices, "double", {'nonnan', 'integer'},...
                                   funcName, argName);
                validRange = [1 this.NumMessages];                
                [minLocalIndices, maxLocalIndices] = bounds(indices);
                invalidRange = minLocalIndices < validRange(1) || ...
                               maxLocalIndices > validRange(2);                
            end
            
            if invalidRange
                error(message('driving:ibeoReader:invalidIndexRange', argName, ...
                              string(validRange(1)), string(validRange(2))));
            end
        end
    end

    methods (Access = 'protected')
        %------------------------------------------------------------------
        % Copy object
        %------------------------------------------------------------------
        function copyObj = copyElement(this)
        % Override copyElement method

            ibeoReader = ibeoFileReader(this.FileName);
            timeRange = [this.StartTime, this.EndTime];
            copyObj = select(ibeoReader, this.MessageType, timeRange, ...
                             'DeviceID', this.DeviceID);
            copyObj.CurrentTime = this.CurrentTime;
        end
    end

    methods (Hidden)
        %------------------------------------------------------------------
        % save object
        %------------------------------------------------------------------
        function that = saveobj(this)
        % save properties into struct
            
            that.FileName      = this.FileName;
            that.MessageType   = this.MessageType;
            that.Timestamps    = this.Timestamps;
            that.StartTime     = this.StartTime;
            that.EndTime       = this.EndTime;
            that.DeviceID      = this.DeviceID;
            that.DeviceIDs     = this.DeviceIDs;
            that.MessagesPos   = this.MessagesPos;
            that.Version       = this.Version;
            that.CurrentTime   = this.CurrentTime;
            
            [that.MessagesInfo, that.PostProcessedObjectsInfo] = ...
            driving.internal.IbeoFileReaderImpl.objectManager(this.FileName,...
                                                              "getFileInfo");
        end

        %------------------------------------------------------------------
        % clear resources
        %------------------------------------------------------------------
        function delete(this)

            driving.internal.IbeoFileReaderImpl.objectManager(this.FileName,...
                                                              "close");
        end
    end

    methods (Static, Hidden)
        %------------------------------------------------------------------
        % load object
        %------------------------------------------------------------------
        function this = loadobj(that)
        % Load Object
            driving.internal.IbeoFileReaderImpl.objectManager(that.FileName,...
                                                              "load", ...
                                                               that.MessagesInfo, ...
                                                               that.PostProcessedObjectsInfo);
            this = ibeoMessageReader(that);
            this.CurrentTime = that.CurrentTime;
        end
    end
end

%--------------------------------------------------------------------------
% helper function to compute 2D bounding box centers from object reference
% point.
%--------------------------------------------------------------------------
function loc = iComputeBoundingBoxCenterPointLocation(x, y, theta, refPointLoc, Bx, By)

% Computes the center point location of the 2D bounding box given the current
% reference point coordinates (x, y), object course angle (theta),
% reference point location (0..9 as per Ibeo Interface Specification v1.50)
% and bounding box sizes (Bx, By).
% The object properties assume a coordinate system centered in the rear
% middle axle of the host vehicle, with the X-axis in the driving direction
% and Y-axis to the left (ISO 8855 / DIN 70000 coordinate system). In this
% coordinate system, the reference point locations as defined below.
% 1: Front/Left
% 2: Front/Right
% 3: Rear/Right
% 4: Rear/Left
% 5: Front/Center
% 6: Right/Center
% 7: Rear/Center
% 8: Left/Center
% 9: Object Center
    theta = single(theta);
    
    % Definition of auxiliary variables.
    delta = atan2(By, Bx);
    D = sqrt(Bx.^2 + By.^2);

    % Compensating depending on the reference point location value.
    actualLocations = unique(refPointLoc);
    if iscolumn(actualLocations)
        actualLocations = actualLocations.';
    end

    for iLoc = actualLocations
        % Logical indexing.
        idx = refPointLoc == iLoc;

        % Applying transformation according to reference point location.
        switch iLoc
            case 1
                % Reference point location is Front/Left.
                x(idx) = x(idx) - D(idx)/2 .* cos(delta(idx) + theta(idx));
                y(idx) = y(idx) - D(idx)/2 .* sin(delta(idx) + theta(idx));
            case 2
                % Reference point location is Front/Right.
                x(idx) = x(idx) - D(idx)/2 .* cos(delta(idx) - theta(idx));
                y(idx) = y(idx) + D(idx)/2 .* sin(delta(idx) - theta(idx));
            case 3
                % Reference point location is Rear/Right.
                x(idx) = x(idx) + D(idx)/2 .* cos(delta(idx) + theta(idx));
                y(idx) = y(idx) + D(idx)/2 .* sin(delta(idx) + theta(idx));
            case 4
                % Reference point location is Rear/Left.
                x(idx) = x(idx) + D(idx)/2 .* cos(delta(idx) - theta(idx));
                y(idx) = y(idx) - D(idx)/2 .* sin(delta(idx) - theta(idx));
            case 5
                % Reference point location is Front/Center.
                x(idx) = x(idx) - Bx(idx)/2 .* cos(theta(idx));
                y(idx) = y(idx) - Bx(idx)/2 .* sin(theta(idx));
            case 6
                % Reference point location is Right/Center.
                x(idx) = x(idx) - By(idx)/2 .* sin(theta(idx));
                y(idx) = y(idx) + By(idx)/2 .* cos(theta(idx));
            case 7
                % Reference point location is Rear/Center.
                x(idx) = x(idx) + Bx(idx)/2 .* cos(theta(idx));
                y(idx) = y(idx) + Bx(idx)/2 .* sin(theta(idx));
            case 8
                % Reference point location is Left/Center.
                x(idx) = x(idx) + By(idx)/2 .* sin(theta(idx));
                y(idx) = y(idx) - By(idx)/2 .* cos(theta(idx));
            case {9, 0}
                % Do nothing as the reference point is Object Center(9) or
                % Center of gravity(0)
            otherwise
                assert(false, sprintf("Invalid reference point location, %d,for the object.", iLoc));
        end

    end

    loc = [x, y];
end

%--------------------------------------------------------------------------
% helper function to decode measurements for measurementList messages.
%--------------------------------------------------------------------------
function [keyName, keyUnit] = iDecodeMeasurementKeyID(key)

switch key
    case 1
        keyName = 'Longitudinal velocity';
        keyUnit = 'm/s';
    case 2
        keyName = 'Yaw rate';
        keyUnit = 'rad/s';
    case 3
        keyName = 'Steering wheel angle';
        keyUnit = 'rad';
    case 4
        keyName = 'Cross acceleration';
        keyUnit = 'm/s^2';
    case 5
        keyName = 'Steering angle';
        keyUnit = 'rad';
    case 6
        keyName = 'Longitudinal acceleration';
        keyUnit = 'm/s^2';
    case 7
        keyName = 'Roll angle';
        keyUnit = 'rad';
    case 8
        keyName = 'Pitch angle';
        keyUnit = 'rad';
    case 43
        keyName = 'Vertical acceleration';
        keyUnit = 'm/s^2';
    case 44
        keyName = 'Pitch rate';
        keyUnit = 'rad/s';
    case 45
        keyName = 'Roll rate';
        keyUnit = 'rad/s';
    case 49
        keyName = 'Velocity north';
        keyUnit = 'm/s';
    case 50
        keyName = 'Velocity west';
        keyUnit = 'm/s';
    case 51
        keyName = 'Velocity up';
        keyUnit = 'm/s';
    case 60
        keyName = 'Latitude';
        keyUnit = 'rad';
    case 61
        keyName = 'Longitude';
        keyUnit = 'rad';
    case 62
        keyName = 'Altitude';
        keyUnit = 'm';
    case 67
        keyName = 'Yaw angle';
        keyUnit = 'rad';
    case 70
        keyName = 'UTC hours';
        keyUnit = '';
    case 71
        keyName = 'UTC minutes';
        keyUnit = '';
    case 72
        keyName = 'UTC seconds';
        keyUnit = '';
    case 73
        keyName = 'UTC milliseconds';
        keyUnit = '';
    case 79
        keyName = 'UTC days';
        keyUnit = '';
    case 80
        keyName = 'UTC months';
        keyUnit = '';
    case 81
        keyName = 'UTC years';
        keyUnit = '';
    otherwise
        keyName = num2str(key);
        keyUnit = 'Unknown';
end

end