classdef polariAngleSpan < handle
    %Angle span measurement direction
    %   Angle spans are measured and graphically filled in the
    %   counter-clockwise direction between two angle markers.
    %
    %   At the command line, angle markers are taken in the order
    %   specified; counter-clockwise angles >180 degrees are preserved.
    %
    %   During graphical interaction, the initial angle span is kept <= 180
    %   degrees by automatic marker ordering.  Angles > 180 degrees are
    %   achieved only by subsequent changes to the span.
    
    % TODO:
    % - delete unused methods (alignWithPolarPlot)

    properties (AbortSet)
        Visible = false
    end
    
    properties (AbortSet)
        Fill = true
    end
    
    properties (Hidden)
        % True when command-line API is used, since that API preserves CCW
        % ordering and allows angle spans exceeding 180 degrees.
        PreserveReflexAngle = true
    end
    
    properties %(Access=private)
        PolariObj
        hAxes
        Listeners
        
        hReadout % internal.polariReadout object
        
        AllCplx = []
        AllIDs = {}
        
        % Struct of change info
        %
        % .dirty: logical scalar, true if any change
        % .modified_IDs  - cell-str
        % .removed_IDs   - cell-str
        % .added_IDs     - cell-str
        % .modified_Cplx - vector corresponding to .modified_IDs
        % .removed_Cplx  - vector
        % .added_Cplx    - vector
        AllLastChange
        
        % These record an ordered set of two SpanIDs and associated complex
        % points.  Span is measured from the first to the second ID /
        % complex value.  If explicitly or automatically swapped, the two
        % are literally swapped to hold this ordering (i.e., that the span
        % is measure from first to second ID / point).
        SpanIDs = {'',''}  % IDs for start and end markers
        SpanCplx = {[] []} % complex cartesian coordinates on unit circle
        
        ZPlane = 0
    end
    
    properties (Dependent)
        % Used by context menu to get/set SpanIDs.  However, unlike
        % SpanIDs, modifying the _LiveUpdate version forces the span to do
        % a graphical update for interactive updating via the context menu.
        SpanIDs_LiveUpdate
    end
    
    properties (Access=private)
        hLocalXform
        hSpan
        
        % Angle span currently shown
        %  - reset to 0 when object is first constructed, or when disabled
        %    (made invisible)
        %  - reset to within 90 degrees of the new value if an initial angle
        %    span > 180 is to be shown; otherwise, automatic reordering
        %    keeps it <= 180 degrees
        pLastSpanCCW = 0
        
        % For span color:
        %
        % Cache dataset index corresponding to each endpoint of the 
        % span; 0 means unset.
        %
        % This tells us when the face color needs updating
        %pSpanFaceColorDatasetIndices = [0 0]
        
        % Helps us shut down adaptive endpoint reordering
        pEndpointsWereChanged = false
    end
    
    properties (Constant)
        % Transparency of span polygon
        SpanAlpha = 0.7
        
        % Maximum # of fraction digits in readout displays (angle or magnitude)
        MaxDigitsDeltaTheta = 4
        MaxDigitsDeltaR = 3  % uses more if integer too big
        
        % True to include "C1 > C3" as first line of readout
        CursorIDsInReadout = true
        
        % Z-plan (depth) of span region
        %   1: behind data always (points, spans, sectors)
        %   2: over data always
        %   3: mouse hover moves span over data
        SpanZPlaneMode = 3
        
        SpanZPlane_UnderData = 0.05 % under points data
        SpanZPlane_OverData = 0.21  % over polygon/sector/intensity data
        
        % Clockwise top semicircle arrow
        %SymbolCW = native2unicode([226 134 183],'UTF-8')
        SymbolCCW = native2unicode([226 134 186],'UTF-8')
        SymbolCW = native2unicode([226 134 187],'UTF-8')
        SymbolDelta = char(916)
        SymbolTheta = char(952)
        SymbolDegree = char(176)
        
        % True forces reset of span to 0 degrees after 360 degrees is
        % passed.  False causes decreasing span angle after 360 degrees is
        % reached.
        ResetAfter360Span = false
        
        %rtArrow = native2unicode([226 134 166],'UTF-8'); % RIGHTWARDS ARROW FROM BAR
        %rtArrow = native2unicode([226 134 146],'UTF-8'); % RIGHTWARDS ARROW
        %rtArrow = native2unicode([226 135 168],'UTF-8'); % RIGHTWARDS WHITE ARROW
        %rtArrow = native2unicode([226 158 161],'UTF-8'); % BLACK RIGHTWARDS ARROW
    end
    
    % Public methods - span
    %
    methods
        function s = polariAngleSpan(p)
            % polariAngleSpan
            if nargin > 0
                init(s,p);
            end
        end
        
        function delete(s)
            % Deleting polariAngleSpan object
            
            deleteAllListeners(s);
            delete(s.hReadout);
            deleteWidgets(s);
        end

        function set.Visible(s,val)
            validateattributes(val,{'logical'}, ...
                {'scalar'},'polariAngleSpan','Visible');
            s.Visible = val;
            
            updateGraphicalSpanVis(s); % side-effect
        end
        
        function set.Fill(s,val)
            validateattributes(val,{'logical'}, ...
                {'scalar'},'polariAngleSpan','Fill');
            s.Fill = val;
            
            updateGraphicalSpanVis(s); % side-effect
        end
        
        function set.SpanIDs_LiveUpdate(s,val)
            % Update the cell-string, then perform graphical update.
            % Must specify cell vector with two IDs strings.
            %
            % Used from context menu to change span start/end marker, and
            % for swapping endpoints.
            
            s.SpanIDs = val;
            changeSpanEndpoints(s);
        end
        
        function val = get.SpanIDs_LiveUpdate(s)
            % Return list of actual (non-truncated) enum strings used for
            % markers.
            
            val = s.SpanIDs;
        end
    end
    
    methods
        % Needed by polari:
        function deleteAllListeners(s)
            
            % Delete all listeners in all substructs in any order:
            s.Listeners = internal.polariCommon.deleteListenerStruct(s.Listeners);
            
            % Delete all listeners, but do so only in this order:
            %s.Listeners.Endpoints = internal.polariCommon.deleteListenerStruct(s.Listeners.Endpoints);
            %s.Listeners.Primary = internal.polariCommon.deleteListenerStruct(s.Listeners.Primary);
        end
        
        function [c,c_display] = getListOfAngleMarkerIDsForEndpoints(s,thisEndpoint)
            % Return cell-string with all angle marker IDs, starting with
            % Cursors and then Peaks, in order.
            %
            % Must specify the endpoint (as start=1 or end=2), so we can
            % filter out the choices.  The startpoint list cannot include
            % the current endpoint, and the endpoint list cannot include
            % the current startpoint.
            
            % thisEndpoint is 1=start or 2=end
            %
            % It is an index into s.SpanIDs or s.SpanCplx
            %
            % If 1 specified, other=2 -- and vice-versa:
            otherEndpoint = 3-thisEndpoint;
            
            % Get list of all angle marker IDs (cursor and peak):
            %p = s.PolariObj;
            %m = [p.hCursorAngleMarkers;p.hPeakAngleMarkers];
            %allIDs = {m.ID};
            allIDs = s.AllIDs;
            
            % Get otherID so we can filter it out of list:
            otherID = s.SpanIDs{otherEndpoint};
            allIDs(strcmpi(otherID,allIDs)) = [];
            
            % Return long names for programmatic use.
            % Shorten the names as well, if only one dataset.
            % Key-sort both lists:
            c = sort(allIDs);
            c_display = modifyIDsForContextMenuDisplay(s,c);
        end
    
        function ids = modifyIDsForContextMenuDisplay(s,ids)
            % For cursor endpoint, just return its ID (e.g., C3).
            % For peaks, return ID if multiple datasets (e.g., P3.1)
            %     while for single datasets, use short form (e.g., P2).
            
            if getNumDatasets(s.PolariObj) == 1
                for i = 1:numel(ids)
                    id_i = ids{i};
                    if strcmpi(id_i(1),'p')
                        ids{i} = id_i(1 : (find(id_i=='.',1)-1));
                    end
                end
            end
        end

        function updateSpanContextMenu(s,hMenu)
            % Dynamic update to angle span context menu.
            
            % Delete old child menus (if any)
            delete(hMenu.Children);
            
            % SPANS
            % C1 -> P3.1
            %
            % Start: C1
            % End: P3.1 or P3 (if only 1 dataset)
            % Delta: 17.1 (32.1deg)
            
            % Modify marker ID for display, so P3.1 shows as P3 when only
            % one dataset is present:
            t = modifyIDsForContextMenuDisplay(s,s.SpanIDs);
            id1 = t{1};
            id2 = t{2};
            
            p = s.PolariObj;
            if strcmpi(p.AngleDirection,'ccw')
                label = ['<html><b>ANGLE SPAN</b><br>' ...
                    '<font size=4>' ...
                    '<i>' id1 ' ' s.SymbolCCW ' ' id2 '</i></font></html>'];
            else
                label = ['<html><b>ANGLE SPAN</b><br>' ...
                    '<font size=4>' ...
                    '<i>' id2 ' ' s.SymbolCW ' ' id1 '</i></font></html>'];
            end
            
            headerOpts = {hMenu,label,'','Enable','off'};
            internal.ContextMenus.createContext(headerOpts);
            
            %topLevel = true;
            %addMeasurementAndLegendMenus(p,hMenu,true,topLevel);
            
            % Span endpoint marker Association
            %
            % "Start" point here is really endpoint (ID2), because we
            % advertise this as CLOCKWISE orientation. "End" point here is
            % really startpoint (ID1).
            %
            % So be careful:
            %  - "Start Point" context uses cell-index 2
            %  - "End Point" context uses cell-index 1
            
            Ncursors = numel(s.PolariObj.hCursorAngleMarkers);
            make = true;
            sep = true;
            [startVals,startVals_display] = getListOfAngleMarkerIDsForEndpoints(s,2);
            h2 = internal.ContextMenus.createContextSubmenu(s,make,sep,hMenu, ...
                'Start Point',startVals_display,{'SpanIDs_LiveUpdate',2}, ...
                startVals);
            % Add separator between cursors and peaks
            if numel(startVals) > Ncursors && Ncursors>0
                h2(Ncursors+1).Separator = 'on';
            end
            
            [endVals,endVals_display] = getListOfAngleMarkerIDsForEndpoints(s,1);
            h2 = internal.ContextMenus.createContextSubmenu(s,make,false,hMenu, ...
                'End Point',endVals_display,{'SpanIDs_LiveUpdate',1}, ...
                endVals);
            % Add separator between cursors and peaks
            if numel(endVals) > Ncursors && Ncursors>0
                h2(Ncursors+1).Separator = 'on';
            end
            
            opts = {hMenu,'Swap Endpoints',@(~,~)m_SwapEndpoints(s),'separator','on'};
            internal.ContextMenus.createContext(opts);
            
            internal.ContextMenus.createContext({hMenu,'Remove Span',  ...
                @(~,~)showAngleSpan(p,false)});
            
            internal.ContextMenus.createContext({hMenu,'Export Span', ...
                @(~,~)m_ExportSpan(s)});
            
            %{
            % Forward/Back menus
            internal.ContextMenus.createContext({hMenu,'Bring to Front', ...
                @(~,~)m_reorderAngleMarker(p,peakID,+1),'separator','on'});
            internal.ContextMenus.createContext({hMenu,'Send to Back', ...
                @(~,~)m_reorderAngleMarker(p,peakID,-1)});
            %}
        end
        
        function changeSpanEndpoints(s,ID1,ID2)
            % Change endpoints to the markers referenced by ID1 and ID2.
            
            % NOTE: this is the service called by .SpanIDs_LiveUpdate, so
            % you can't set that property here - it would cause infinite
            % recursion.  Not good.
            
            passedIDs = nargin > 1;
            if passedIDs
                % New IDs passed in as strings ID1 and ID2
                spanIDs = {ID1,ID2};
            else
                % Get existing span endpoint IDs, assuming they were
                % recently changed and we need to react to them as if they
                % were passed in as args
                spanIDs = s.SpanIDs;
            end
            
            % s.AllIDs is assumed to be up-to-date here
            %
            % Check that IDs are valid, and find indices in master list
            [present,locb] = ismember(upper(spanIDs),s.AllIDs);
            if ~all(present)
                if ~present(1)
                    invalidID = spanIDs{1}; % ID1;
                else
                    invalidID = spanIDs{2}; % ID2;
                end
                error('Invalid marker ID "%s".',invalidID);
            end
            if locb(1) == locb(2)
                if ~passedIDs
                    % New IDs were not passed in, so these IDs are already
                    % in SpanIDs.  That's trouble, because they are
                    % invalid.  Revert them here.  Or shut down the span.
                    %
                    s.SpanIDs = {'',''};
                    s.SpanCplx = {[],[]};
                    hideSpan(s);
                end
                error('Distinct markers must be specified for each span endpoint.');
            end
            
            if passedIDs
                % Record new markers to use:
                s.SpanIDs = spanIDs;
            end
            
            % Copy the corresponding complex values from AllCplx:
            s.SpanCplx = {s.AllCplx(locb(1)), s.AllCplx(locb(2))};
            
            % Do the graphical update:
            % - we need to signal that the change came from a "discrete"
            %   change to the span endpoints, and not a "continuous" change
            %   to the span cursors.  This helps us know whether to do
            %   adaptive endpoint reordering.
            s.pEndpointsWereChanged = true;
            updateGraphicalSpanVis(s);
            s.pEndpointsWereChanged = false;
        end
        
        function revertSpanEndpoints(s)
            % Revert span visual to stored current endpoints.
            updateWidgets(s);
        end
        
        function m_SwapEndpoints(s)
            % Swap start and end points of span, such that the angle span
            % will now be 360 degrees minus the former span.
            
            % Cannot swap endpoints if both are not present
            if ~any(cellfun(@isempty,s.SpanIDs))
                %o = s.PreserveReflexAngle;
                s.PreserveReflexAngle = true;
                
                % Does a graphical update
                s.SpanIDs_LiveUpdate = {s.SpanIDs{2},s.SpanIDs{1}};
                
                % Restore reflex angle setting
                %s.PreserveReflexAngle = o;
            end
        end
        
        function m_ExportSpan(s)
            % Export span info to base workspace.
            % Exports a structure with the fields:
            %
            %  .angleDiff
            %      Clockwise difference in angle between start and end
            %      markers of the span, in the range 0 to 360 degrees.
            %  .magnitudeDiff
            %      Magnitude difference between start and end markers of
            %      the span.
            %  .markers
            %      2-element vector of structures describing angle markers
            %      at the counterclockwise and clockwise ends of the span.
            %      The structure is the same as returned by the
            %      AngleMarkers property of polari.
            
            % Export span info to base workspace:
            assignin('base','span',spanDetails(s));
            str = 'Exported <a href="matlab:eval(''span'')">span</a> variable to the base workspace.';
            disp(str);
            showBannerMessage(s.PolariObj, ...
                'Exported ''span'' variable to the base workspace.');
        end
        
        function y = spanDetails(s)
            % Returns a structure containing span details, with the fields:
            %  .angleDiff
            %      Clockwise difference in angle between start and end
            %      markers of the span, in the range 0 to 360 degrees.
            %  .angleUnits
            %      Units of angle
            %  .magnitudeDiff
            %      Magnitude difference between start and end markers of
            %      the span.
            %  .magnitudeUnits
            %      Units of magnitude, empty if none specified in
            %      MagnitudeUnits property of polarpattern object.
            %  .intensityDiff
            %      Intensity difference between start and end markers of
            %      the span; empty unless dataset is 3D (intensity).
            %  .intensityUnits
            %      Units of intensity, empty if none specified in
            %      IntensityUnits property of polarpattern object.
            %  .markers
            %      2-element vector of structures describing angle markers
            %      at the counterclockwise and clockwise ends of the span.
            %      The structure is the same as returned by the
            %      AngleMarkers property of polarpattern.
            
            p = s.PolariObj;
            if isempty(s)
                % An empty span object (e.g., no span object)
                % Use defaults:
                minfo = markerInfo(internal.polariAngleMarker.empty);
                adiff = [];
                mdiff = [];
                idiff = [];
            else
                % Valid span object, but may contain no span
                sID1 = s.SpanIDs{1};
                sID2 = s.SpanIDs{2};
                if ~s.Visible || isempty(sID1) || isempty(sID2)
                    % Use defaults:
                    minfo = markerInfo(internal.polariAngleMarker.empty);
                    adiff = [];
                    mdiff = [];
                    idiff = [];
                else
                    % Compute values:
                    m2 = findAngleMarkerByID(p,s.SpanIDs{2});
                    m1 = findAngleMarkerByID(p,s.SpanIDs{1});
                    minfo = markerInfo([m1;m2]);
                    
                    % Delta computations:
                    is_ccw = strcmpi(p.AngleDirection,'ccw');
                    
                    adiff = internal.polariCommon.cangleDiff( ...
                        s.SpanCplx{1},s.SpanCplx{2}) * 180/pi;
                    
                    if is_ccw
                        % id1 CCW id2 -> m2-m1
                        mdiff = minfo(2).magnitude - minfo(1).magnitude;
                        idiff = minfo(2).intensity - minfo(1).intensity;
                    else
                        % id2 CW  id1 -> m1-m2
                        mdiff = minfo(1).magnitude - minfo(2).magnitude;
                        idiff = minfo(1).intensity - minfo(2).intensity;
                    end
                end
            end
            aunits = 'deg';
            munits = p.MagnitudeUnits;
            iunits = p.IntensityUnits;

            y = struct( ...
                'angleDiff',adiff, ...
                'angleUnits',aunits, ...
                'magnitudeDiff',mdiff, ...
                'magnitudeUnits',munits, ...
                'intensityDiff',idiff, ...
                'intensityUnits',iunits, ...
                'markers',minfo);
        end
        
        function init(s,p)
            % Initialize polariAngleSpan.
            
            s.PolariObj = p;
            s.hAxes = p.hAxes;
            
            % Initial span will be kept to <= 180 degrees.
            % Must be reset when reinitializing or making visible.
            %
            % Set to within 90 degrees of a new angle span in order to
            % display initial spans > 180 degrees.
            s.pLastSpanCCW = 0;
            
            initListeners(s);
            updateAngleMarkersList(s);
            changedPolarStyle(s);
            initReadout(s);
        end
        
        function initLurking(s,p)
            % Initialize where no markers are present, and we want span to
            % appear once at least 2 markers are added.  This is the same
            % mode of operation as normal init, in which 2 markers were
            % initially created then removed.  That is the "lurking" state.
            
            init(s,p);
            createWidgets(s);
            s.hLocalXform.Visible = 'on';
            s.Visible = true;
            updateWidgets(s);
        end
    
        function updateAngleMarkersList(s)
            % Peak and/or Cursor angle marker vectors changed,
            % meaning markers were REMOVED or ADDED.
            %
            % Try to hold the two endpoints at the same marker IDs, since
            % marker IDs don't change unless removed AND another cursor is
            % added with that ID.  That would be TWO steps however, never
            % one.  So we won't find "reuse" as long as we don't miss an
            % update call.
            %
            % If an endpoint marker is removed, we must re-associate that
            % endpoint with the closest marker in angle-space.
            
            rebuildMarkerList(s);
            anyChange = selectNewEndpointsIfEmpty(s);
            
            if anyChange
                updateGraphicalSpanVis(s);
            else
                updateWidgets(s);
            end
        end
        
        function updatePosition(s,newPos)
            % Update position of readout associated with the span
            
            hr = s.hReadout;
            if ~isempty(hr)
                updatePosition(hr,newPos);
            end
        end
        
        function hoverOverReadoutChange(s,event)
            % Called when mouse is hovering over readout associated with
            % span (not span itself).
            %
            % Events: 'start', 'stop'
            hr = s.hReadout;
            if ~isempty(hr)
                hoverOverReadoutChange(hr,event);
            end
        end
        
        function hoverOverSpanChange(s,action)
            % Called when mouse is hovering over span (not readout).
            %
            % action: 'start', 'stop'
            
            % Change z-depth of span
            p = s.PolariObj;
            
            if isIntensityData(p)
                z0 = s.SpanZPlane_OverData;
                s.ZPlane = z0;
                updateWidgets(s);
                
            elseif ~strcmpi(p.Style,'line')
                if strcmpi(action,'start')
                    z0 = s.SpanZPlane_OverData;
                else
                    z0 = s.SpanZPlane_UnderData;
                end
                s.ZPlane = z0;
                updateWidgets(s);
            end
        end
    end
    
    methods (Access=private)
        function initReadout(s)
            % Initialize readout
            
            hr = internal.polariReadout(s.PolariObj);
            s.hReadout = hr;
            hr.FontName = 'Courier';
            hr.FontRelSize = -2;
            hr.TagName = 'polariSpanReadout';
            hr.ContextMenuFcn = @(hMenu,ev)updateReadoutContextMenu(s,hMenu);
        end
        
        function updateReadoutContextMenu(s,hMenu)
            % Span readout context menu.
            %
            % Dynamically invoked when context menu is opened.
            
            % Delete old child menus (if any)
            delete(hMenu.Children);
            
            % SPAN
            % C1 -> P3.1
            %
            % Start: C1
            % End: P3.1 or P3 (if only 1 dataset)
            % Delta: 17.1 (32.1deg)
            
            % Modify marker ID for display, so P3.1 shows as P3 when only
            % one dataset is present:
            ids = modifyIDsForContextMenuDisplay(s,s.SpanIDs);
            id1 = ids{1};
            id2 = ids{2};
            
            p = s.PolariObj;
            if strcmpi(p.AngleDirection,'ccw')
                label = ['<html><b>ANGLE SPAN</b><br>' ...
                    '<font size=4>' ...
                    '<i>' id1 ' ' s.SymbolCCW ' ' id2 '</i></font></html>'];
            else
                label = ['<html><b>ANGLE SPAN</b><br>' ...
                    '<font size=4>' ...
                    '<i>' id2 ' ' s.SymbolCW ' ' id1 '</i></font></html>'];
            end
            
            headerOpts = {hMenu,label,'','Enable','off'};
            internal.ContextMenus.createContext(headerOpts);

            opts = {hMenu,'Swap Endpoints',@(~,~)m_SwapEndpoints(s),'separator','on'};
            internal.ContextMenus.createContext(opts);

            opts = {hMenu,'Remove Span', ...
                @(~,~)showAngleSpan(p,false)};
            internal.ContextMenus.createContext(opts);
            
            internal.ContextMenus.createContext({hMenu,'Export Span', ...
                @(~,~)m_ExportSpan(s)}); % ,'separator','on'});
        end

        function initListeners(s)
            % Listen to marker properties in polari 
            %
            % We make two substructs since we create and delete these
            % subsets at different times.
            
            deleteAllListeners(s);
            p = s.PolariObj;
            
            % Primary Listeners
            % -----------------
            %
            % Listen for a change to the marker lists
            lis.Primary.AngleMarkersListListener = addlistener(p, ...
                {'hCursorAngleMarkers','hPeakAngleMarkers'}, ...
                'PostSet', @(~,~)updateAngleMarkersList(s));
            
            % angle ticks were rotated
            lis.Primary.PolariAngleProps = addlistener(p, ...
                {'AngleAtTop','AngleDirection','AngleDrag_Delta'}, ...
                'PostSet',@(~,~)angleTicksMoved(s));
            
            % Magnitude unit string was changed
            lis.Primary.PolariMagUnits = addlistener(p, ...
                'MagnitudeUnits', ...
                'PostSet', @(~,~)updateReadout(s));
            
            % when Polari style property is changed, update z-depth setting
            lis.Primary.PolariStyle = addlistener(p, ...
                'Style','PostSet',@(~,~)changedPolarStyle(s,true));
            
            % Endpoint Listeners
            % ------------------
            % Listen for changes to the two span endpoint markers, but only
            % when selected.  These listeners are created separately.
            %
            lis.Endpoints.StartMarker = [];
            lis.Endpoints.StartMarkerActiveTraceChange = [];
            lis.Endpoints.EndMarker = [];
            lis.Endpoints.EndMarkerActiveTraceChange = [];
            
            s.Listeners = lis;
        end
        
        function changedPolarStyle(s,forceUpdate)
            % React to a change in Style ('filled','line',etc) of the
            % Polari object
            
            p = s.PolariObj;
            
            if isIntensityData(p)
                z0 = s.SpanZPlane_OverData;
            elseif strcmpi(p.Style,'line')
                z0 = s.SpanZPlane_UnderData;
            else % filled, sectors
                z0 = s.SpanZPlane_OverData;
            end
            s.ZPlane = z0;
            
            if (s.ZPlane ~= z0) && nargin>1 && forceUpdate && s.Visible
                updateWidgets(s);
            end
        end

        function rebuildMarkerList(s)
            % Create marker list from Cursor and Peak markers,
            % and compute associated changes.
            
            % Determine what changed BEFORE updating .AllIDs and .AllCplx
            %
            % Assumptions:
            % - Order of markers in lists is unimportant
            %   (No changes are reported for reordering)
            % - No duplicate markers exist within a list
            
            % Get new marker info
            %
            p = s.PolariObj;
            m = [p.hCursorAngleMarkers;p.hPeakAngleMarkers];
            if isempty(m)
                newIDs = {};
                newCplx = [];
            else
                th = getNormalizedAngle(p,getAngleFromVec(m)); % CCW, 0-deg is +x
                th = mod(th,2*pi);   % force all to positive radians
                [th,idx] = sort(th); % xxx is this needed?
                newCplx = complex(cos(th),sin(th));
                newIDs = {m(idx).ID};
            end
            
            % Identify changes
            %
            oldIDs  = s.AllIDs;
            oldCplx = s.AllCplx;
            
            [isPresent,firstMatchIdxInNew] = ismember(oldIDs,newIDs);
            removedIDs = oldIDs(~isPresent);
            removedCplx = oldCplx(~isPresent);
            % indices into newIDs and newCplx, where new IDs match old IDs
            newIdx = firstMatchIdxInNew(isPresent);
            oldIdx = find(isPresent);
            
            % Position of entries in oldIdx and newIdx identify
            % corresponding entries in old and new lists.
            %
            % Find all same-ID entries with differing cplx values:
            %assert(numel(oldIdx) == numel(newIdx))
            modifiedIDs = {};
            modifiedCplx = [];
            if ~isempty(oldIdx)
                oldCplx_commonIDs = oldCplx(oldIdx);
                newCplx_commonIDs = newCplx(newIdx);
                sel = oldCplx_commonIDs ~= newCplx_commonIDs;
                if any(sel)
                    assert(isequal(oldIDs(oldIdx(sel)),newIDs(newIdx(sel))),'changed markers must have the same ID')
                    modifiedIDs = oldIDs(oldIdx(sel)); % equiv: newIDs(newIdx(sel))
                    modifiedCplx = {oldCplx_commonIDs,newCplx_commonIDs};
                end
            end
            
            info.removedIDs   = removedIDs;
            info.removedCplx  = removedCplx;
            info.modifiedIDs  = modifiedIDs;
            info.modifiedCplx = modifiedCplx; % 2-element cell-vector, {oldVals,newVals}
            info.dirty = ~isempty(removedIDs) || ~isempty(modifiedIDs);
            
            assert(~info.dirty || ...
                (~isequal(newIDs,s.AllIDs) || ~isequal(newCplx,s.AllCplx)))
            
            % Store new lists
            s.AllIDs = newIDs;
            s.AllCplx = newCplx;
            s.AllLastChange = info;
        end
        
        function anyChange = selectNewEndpointsIfEmpty(s)
            % Confirm we have two non-empty SpanIDs as endpoints.
            %
            % If two SpanIDs are present, all set
            %
            % If fewer than 2 markers total are available, stop
            %
            % If only one SpanID is present,
            %   No reordering of 1 and 2 allowed
            %
            %   If complex value for previous marker exists,
            %      Choose marker closest to previous angle (CW or CCW)
            %
            %   If complex value empty,
            %      If SpanIDs(1) is empty, choose marker closest to 2
            %      If SpanIDs(2) is empty, choose marker closest to 1
            %
            % If both SpanIDs are empty,
            %   If complex value for previous markers exist,
            %      Choose angles closest to each last angle
            %
            %   If both complex values are empty,
            %      If only 2 markers present, choose them in order that
            %           creates the smallest subtended angle
            %      If 3 or more markers present, choose adjacent pair
            %         ordered to create smallest subtended angle
            %         If only Peaks present, P1 and P2
            %         If only Cursors present, C1 and C2
            %         If both Peaks and Cursors present, 
            
            c = s.AllCplx;
            [isInSet,idx] = ismember(s.SpanIDs,s.AllIDs);
            
            % update possibly-stale local marker values
            if ~isInSet(1)
                c1 = [];
            else
                c1 = c(idx(1));
                s.SpanCplx{1} = c1;
            end
            if ~isInSet(2)
                c2 = [];
            else
                c2 = c(idx(2));
                s.SpanCplx{2} = c2;
            end
            new_cplx = {c1 c2};
            
            % Contains UPDATED span endpoint values,
            % but does not clear out REMOVED values.
            c_prior = s.SpanCplx;
            
            % clear out values if valid marker not in master list
            % (master list is stale)
            empty_ids = cellfun(@isempty,s.SpanIDs);
            if ~empty_ids(1) && ~isInSet(1)
                s.SpanCplx{1} = [];
                s.SpanIDs{1} = '';
            end
            if ~empty_ids(2) && ~isInSet(2)
                s.SpanCplx{2} = [];
                s.SpanIDs{2} = '';
            end
            
            % If any complex value is empty, we will try to associate a new
            % marker with that endpoint.  Assuming N>=2 of course.
            if ~isempty(c1) && ~isempty(c2)
                anyChange = ~isequal(c_prior,new_cplx);
                if ~anyChange
                    return
                end
            else
                anyChange = true;
            end
            
            % If we reset either span ID above, we made a change that we
            % want to track here.  Resetting means a marker went away, and
            % now an endpoint is no longer available.  We need to find a
            % new one.  That's the change we seek here.
            %
            % Note that either span ID could be empty and yet we haven't
            % changed anything.  We ignore that, it's not a change.
            % There's nothing to do in that case.
            %
            % If marker list changed, but all IDs remain valid, there is no
            % reason to find new IDs so we exit.
            %changedGlobalIDs = markerListChangeInfo.changed_IDs;
            
            % True means there was a change to cplx values in global marker
            % list, but not necessarily in the two complex values
            % associated with the start/end points:
            %changedGlobalCplx = markerListChangeInfo.changed_Cplx;
            
            % .AllCplx represents normalized coordinates on the unit
            % circle.  0-deg is "right", and CCW is +ve dir.
            %
            % "Up" is at +90 degrees.
            N = numel(c);
            if N < 2
                % At least 2 markers must be present in marker list
                %
                % Keep anyChange flag, since markers may have been removed
                % and we now have < 2 markers
                return
            end
            
            % May have reset IDs now:
            empty_ids = cellfun(@isempty,s.SpanIDs);
            if all(empty_ids)
                % Both SpanIDs are empty -> neither marker is selected
                %
                c1 = c_prior{1};
                c2 = c_prior{2};

                % xxx consider using internal.polariCommon.cangleDiff for all of
                % these angle differences.  Here we get lengths of chords
                % (straight lines between the two points), whereas we
                % really want angle-diffs.
                
                if ~isempty(c1) && ~isempty(c2)
                    % Both prior marker angles are known
                    ang_diff = abs(c - c1);
                    [~,i1] = min(ang_diff);
                    c(i1) = inf; % don't choose this again!
                    ang_diff = abs(c - c2);
                    [~,i2] = min(ang_diff);
                    
                elseif ~isempty(c1)
                    % c1 prior angle is known, c2 is not
                    %
                    % Choose i1 closest to angle(c1) ... i.e., to c1
                    % Choose i2 closest to i1 ... i.e., to c1
                    ang_diff = abs(c - c1);
                    [~,i1] = min(ang_diff);
                    c1 = c(i1);  % actual angle (cplx value)
                    c(i1) = inf; % don't choose this again!
                    ang_diff = abs(c - c1);
                    [~,i2] = min(ang_diff);
                    
                elseif ~isempty(c2)
                    % c2 prior angle is known, c1 is not
                    %
                    % Choose i2 closest to angle(c2)
                    % Choose i1 closest to i2
                    ang_diff = abs(c - c2);
                    [~,i2] = min(ang_diff);
                    c2 = c(i2);  % actual angle (cplx value)
                    c(i2) = inf; % don't choose this again!
                    ang_diff = abs(c - c2);
                    [~,i1] = min(ang_diff);
                    
                else
                    % neither prior angle is known
                    %
                    if N == 2
                        % If only 2 markers present, choose them in order
                        % that creates the smallest subtended angle in CCW
                        % sense.
                        %
                        % Cross-product (right-hand rule):
                        %  - "1 cross 2" produces +z if CCW order
                        if internal.polariCommon.isCW(c(1),c(2))
                            i1 = 2;
                            i2 = 1;
                        else
                            i1 = 1;
                            i2 = 2;
                        end
                    else
                        % If 3 or more markers present, choose adjacent
                        % pair closest to "north", which is 90-deg in
                        % normalized angle.
                        
                        % Express range from -90 to +270 (that is, from
                        % 90-180 to 90+180), such that no angles "wrap"
                        % when compared with 90 deg:
                        
                        c0 = complex(0,1); % =complex(cos(pi/2),sin(pi/2))
                        ang_diff = internal.polariCommon.cangleAbsDiff(c0,c);
                        [~,i1] = min(ang_diff);
                        ang_diff(i1) = inf;
                        [~,i2] = min(ang_diff);
                        
                        % Do CCW-oriented diff between these two points.
                        %
                        if internal.polariCommon.cangleDiff(c(i1),c(i2)) > pi
                            % Swap endpoints so they subtend the smallest
                            % of two possible angles
                            %
                            t = i1; i1 = i2; i2 = t;
                        end
                    end
                end
                
                % Record new markers to use in place of missing markers:
                idx = [i1 i2];
                s.SpanIDs = s.AllIDs(idx);
                s.SpanCplx = {c(idx(1)), c(idx(2))};
                
            elseif ~any(empty_ids)
                % Both IDs still present
                %
                % At least one of the endpoint cplx values must have
                % changed
                %
                % Record new marker to use in place of missing marker:
                
                s.SpanCplx = new_cplx;

            else
                % One SpanID is present
                % No reordering of 1 and 2 allowed

                % Determine if endpoint 1 or 2 is empty
                %
                % enum is either 1 (for start) or 2 (for end):
                enum_missing_marker  = find(empty_ids);
                assert(isscalar(enum_missing_marker))
                enum_existing_marker = 3-enum_missing_marker; % 1->2, 2->1
                idx_existing_marker  = idx(enum_existing_marker);
                
                % Replacement marker should be closest to the angle of the
                % missing marker, but if that angle is unknown, it should
                % be closest to the existing marker.
                %
                target_c = c_prior{enum_missing_marker};
                if isempty(target_c)
                    % Missing marker also has missing angle
                    target_c = c_prior{enum_existing_marker};
                %else
                    % Missing marker has known angle
                end
                
                % Sort available markers into CW and CCW lists, computing
                % distance from target_c. Truncate lists at this
                % marker and at other marker.  All normalized.
                
                % Prepare CCW-ordered list of angle markers
                % - remove all markers at/after the other endpoint
                % - list could become empty
                [ccw_dist,cw_dist] = internal.polariCommon.cangleDiff(target_c,c);
                [~,ccw_sort_i] = sort(ccw_dist);
                j = find(ccw_sort_i == idx_existing_marker);
                ccw_sort_i = ccw_sort_i(1:j-1);
                
                % Prepare CW-ordered list of angle markers
                % - remove all markers at/after the other endpoint
                % - list could become empty
                [~,cw_sort_i] = sort(cw_dist);
                j = find(cw_sort_i == idx_existing_marker);
                cw_sort_i = cw_sort_i(1:j-1);
                
                if isempty(ccw_sort_i)
                    % only consider cw_dist, take closest
                    idx_replacement_marker = cw_sort_i(1);
                    
                elseif isempty(cw_sort_i)
                    % only consider ccw_dist, take closest
                    idx_replacement_marker = ccw_sort_i(1);
                    
                else
                    % Consider both cw_dist and ccw_dist, choose
                    % closest:
                    closest_ccw_idx  = ccw_sort_i(1);
                    closest_ccw_dist = ccw_dist(closest_ccw_idx);
                    closest_cw_idx   = cw_sort_i(1);
                    closest_cw_dist  = cw_dist(closest_cw_idx);
                    if closest_ccw_dist < closest_cw_dist
                        idx_replacement_marker = closest_ccw_idx;
                    else
                        idx_replacement_marker = closest_cw_idx;
                    end
                end
                
                % Record new marker to use in place of missing marker:
                s.SpanIDs{enum_missing_marker} = s.AllIDs{idx_replacement_marker};
                s.SpanCplx{enum_missing_marker} = c(idx_replacement_marker);
            end
            
            % DEBUG:
            %fprintf('xxx %.1f deg -> %.1f deg (normalized)\n', ...
            %    angle(s.SpanCplx{1})*180/pi, ...
            %    angle(s.SpanCplx{2})*180/pi);
        end
        
        function addListenersOnEndpoints(s)
            
            deleteListenersOnEndpoints(s);
            
            lis = s.Listeners;
            p = s.PolariObj;
            
            m = findAngleMarkerByID(p,s.SpanIDs{1});
            lis.Endpoints.StartMarker = addlistener(m, ...
                'MarkerChanged', ...
                @(~,~)updateSpan(s));
            lis.Endpoints.StartMarkerActiveTraceChange = addlistener(m, ...
                'MarkerChangedActiveTrace', ...
                @(~,~)updateActiveTraceChange(s));
            
            m = findAngleMarkerByID(p,s.SpanIDs{2});
            lis.Endpoints.EndMarker = addlistener(m, ...
                'MarkerChanged', ...
                @(~,~)updateSpan(s));
            lis.Endpoints.EndMarkerActiveTraceChange = addlistener(m, ...
                'MarkerChangedActiveTrace', ...
                @(~,~)updateActiveTraceChange(s));
            
            s.Listeners = lis;
        end
        
        function deleteListenersOnEndpoints(s)
            % Delete only Endpoints listeners
            s.Listeners.Endpoints = ...
                internal.polariCommon.deleteListenerStruct(s.Listeners.Endpoints);
        end
    
        function updateActiveTraceChange(s)
            % Only perform activities that updateSpan() would miss, 
            % like a change in active dataset index in markers whose mode
            % is 'auto'
            updateSpanFaceColor(s);
        end
        
        function updateGraphicalSpanVis(s)
            % Update graphical span.
            
            if s.Visible % user-intent, but may not be possible
                e = cellfun(@isempty,s.SpanIDs);
                if any(e)
                    hideSpan(s);
                else
                    showSpan(s);
                end
            else
                hideSpan(s);
            end
        end
        
        function showSpan(s)

            addListenersOnEndpoints(s);
            if isempty(s.hSpan)
                createWidgets(s);
            end
            s.hLocalXform.Visible = 'on';
            
            updateWidgets(s);
            
            if ~isempty(s.hReadout)
                s.hReadout.Visible = 'on';
            end
        end
        
        function hideSpan(s)
            
            % If no data is present, no hLocalXform was made:
            if ~isempty(s.hLocalXform)
                s.hLocalXform.Visible = 'off';
            end
            
            deleteListenersOnEndpoints(s);
            
            if ~isempty(s.hReadout)
                s.hReadout.Visible = 'off';
            end
        end
        
        function updateSpan(s)
            % One marker in span changed index (etc),
            % but didn't add/remove itself from list.
            
            % This does more than needed:
            %  - it assumes the marker list was updated
            updateAngleMarkersList(s);
            
            % xxx this does too little
            %   it doesn't update datadot lines, etc
            %updateWidgets(s);
        end
        
        function angleTicksMoved(s)
            % Call for changes to:
            %  .DataIndex
            %  .Z
            %  .PolariObj.pData
            %  .PolariObj.AngleAtTop
            %  .PolariObj.AngleDrag_Delta
            %  .PolariObj.AngleDirection
            %  .PolariObj.AngleRange
            
            % Do the graphical update:
            % - we need to signal that the change came from a "discrete"
            %   change to the span endpoints, and not a "continuous" change
            %   to the span cursors.  This helps us know whether to do
            %   adaptive endpoint reordering.
            s.pEndpointsWereChanged = true;
            updateAngleMarkersList(s);
            s.pEndpointsWereChanged = false;
        end
        
        function alignWithPolarPlot(s)
            % Rotate for angle tick (re)alignment.
            % Set Zplane.
            
            hgt = s.hLocalXform;
            if ~isempty(hgt) % && ishghandle(hgt)
                % Translate angle from display space to normalized angle
                p = s.PolariObj;
                
                thd = 90 - 90 - p.AngleAtTop - p.AngleDrag_Delta;
                if strcmpi(p.AngleDirection,'ccw')
                    thd = 180-thd;
                end
                th = -thd*pi/180;
                
                % Rotate and translate the transform parent of the marker
                % patch and the marker text
                %
                % transform = T*R = translate * rotate
                ct = cos(th);
                st = sin(th);
                hgt.Matrix = [ ...
                    -ct  st 0 0; ...
                    -st -ct 0 0; ...
                      0   0 1 s.ZPlane; ...
                      0   0 0 1];
            end
        end
    end
    
    methods (Hidden)
        function tempChangeToSpanColorAndReadout(s,m1,m2)
            % Temporary change to span color during interactive change to
            % endpoints.  Also update readout.
            updateSpanFaceColor(s, ...
                [getDataSetIndex(m1), getDataSetIndex(m2)]);
            
            updateReadout(s,m1,m2);
        end
        
        function tempChangeToSpanEndpoints(s,new_angles)
            % Temporary change to span endpoints during interactive
            % change to endpoints.
            %
            % new_angles is a 2-element vector of angles, indicating the
            % new temporary startAngle and endAngle.
            
            [verts,faces] = internal.polariCommon.sectorsPatchRounded( ...
                1.0,new_angles,s.ZPlane);
            
            set(s.hSpan, ...
                'Faces',faces, ...
                'Vertices',verts);
        end
    end
    
    methods (Access=private)
        function updateReadout(s,m1,m2)
            % Update readout text based on SpanCplx and SpanIDs
            %
            % updateReadout(s)
            % updateReadout(s,isFullCircle)
            % updateReadout(s,m1,m2)
            %
            % m1 and m2 are markers.
            % Angle span should be CCW from m1 to m2.
            
            p = s.PolariObj;
            if nargin == 3
                % updateReadout(s,m1,m2)
                % - have m1, m2
                % - need c1, c2
                c1 = getComplexAngle(m1);
                c2 = getComplexAngle(m2);
                % Measure CCW angle from c1 to c2
                adiff = internal.polariCommon.cangleDiff(c1,c2) * 180/pi;
            else
                % updateReadout(s)
                % updateReadout(s,isFullCircle)
                %
                if (nargin>1) && m1
                    % updateReadout(s,isFullCircle)
                    % - 2nd arg passed: m1 is really "isFullCircle",
                    %   and it's true here
                    % - used for AngleLim cursors, when they are both
                    %   on top of each other, and we want to show 360
                    adiff = 360; % Exact full-circle, NOT zero degrees
                else
                    % Measure CCW angle from c1 to c2
                    c1 = s.SpanCplx{1};
                    c2 = s.SpanCplx{2};
                    adiff = internal.polariCommon.cangleDiff(c1,c2) * 180/pi;
                end
                m1 = findAngleMarkerByID(p,s.SpanIDs{1});
                m2 = findAngleMarkerByID(p,s.SpanIDs{2});
            end
            
            is_ccw = strcmpi(p.AngleDirection,'ccw');
            
            % Mag diff:
            d1 = getData(m1);
            d2 = getData(m2);
            if is_ccw
                % id1 CCW id2 -> d2-d1
                mdiff = d2.mag - d1.mag;
            else
                % id2 CW id1 -> d1-d2
                mdiff = d1.mag - d2.mag;
            end
            
            % Intensity diff (for 3D data)
            if isempty(d1.intensity)
                idiff = [];
            else
                if is_ccw
                    % id1 CCW id2 -> d2-d1
                    idiff = d2.intensity - d1.intensity;
                else
                    % id2 CW id1 -> d1-d2
                    idiff = d1.intensity - d2.intensity;
                end
            end
            
            % Use engineering-units scaling for display of mag diff
            if isnan(mdiff) % engunits doesn't handle nan
                scale = 1;
                mksUnit = '';
            else
                [mdiff,scale,mksUnit] = engunits(mdiff,'unicode');
            end
            
            % Check for user-specified mag units, possibly with an embedded
            % "%s" or "%c" conversion string
            uStr = p.MagnitudeUnits;
            if isempty(uStr) && scale==1
                units = '';
            else
                % use MKS unit string
                if isempty(uStr)
                    % No user label
                    % Just use a numerical scale factor
                    % Compute power of 10
                    units = sprintf('e%g',log10(1/scale));
                else
                    % use MKS unit string
                    if any(uStr=='%')
                        % User included units format: '%s' or '%c'
                        units = sprintf(uStr,mksUnit);
                    else
                        % Prepend MKS unit to user suffix
                        units = [mksUnit uStr];
                    end
                end
            end
            
            % Delta theta X degrees
            %
            %  - when converting degrees to radians, subtracting, then
            %  converting back to degrees, roundoff occurs.  Running
            %  through all integers -180:180 and 0:360 and doing the above,
            %  we find a max of ~512 eps.  So use a limit of 1000 eps for
            %  roundoff, or 2e-13 -> use 1e-12.
            adiff = round(adiff*1e12)*1e-12;
            tmp = internal.polariCommon.sprintfMaxNumTotalDigits(adiff,s.MaxDigitsDeltaTheta);
            s1 = sprintf('%c%c %s%s',s.SymbolDelta,s.SymbolTheta,tmp,s.SymbolDegree);
            
            % Delta magnitude X <units>
            tmp = internal.polariCommon.sprintfMaxNumTotalDigits(mdiff,s.MaxDigitsDeltaR);
            s2 = sprintf('%cm %s%s',s.SymbolDelta,tmp,units);
            
            % Delta intensity X
            if isempty(idiff)
                s3 = '';
            else
                [idiff,scale,mksUnit] = engunits(idiff,'unicode');
                
                % check for intensity units
                uStr = p.IntensityUnits;
                if isempty(uStr) && scale==1
                    units = '';
                else
                    % use MKS unit string
                    if isempty(uStr)
                        % No user label
                        % Just use a numerical scale factor
                        % Compute power of 10
                        units = sprintf('e%g',log10(1/scale));
                    else
                        % use MKS unit string
                        if any(uStr=='%')
                            % User included units format: '%s' or '%c'
                            units = sprintf(uStr,mksUnit);
                        else
                            % Prepend MKS unit to user suffix
                            units = [mksUnit uStr];
                        end
                    end
                end
                
                tmp = internal.polariCommon.sprintfMaxNumTotalDigits(idiff,s.MaxDigitsDeltaR);
                s3 = sprintf('%ci %s%s',s.SymbolDelta,tmp,units);
            end
            
            if s.CursorIDsInReadout
                % Include Cursor IDs in readout.
                % Must use m1,m2 to reflect interactive changes:
                t = modifyIDsForContextMenuDisplay(s,{m1.ID,m2.ID});
                
                % Style 1: Extended ASCII, C1 ~ C2
                % s0 = sprintf('%s%c%s',t{1},s.SymbolCW,t{2});
                %
                % Style 2: Vector notation, C1 -> C2
                % s0 = sprintf('%s > %s',t{1},t{2});
                %
                % Style 3: Difference notation, C2 - C1
                %    (vector C1 -> C2 means we compute C2-C1)
                if is_ccw
                    % id1 CCW id2 -> id2-id1
                    s0 = sprintf('%s - %s',t{2},t{1});
                else
                    % id2 CW id1 -> id1-id2
                    s0 = sprintf('%s - %s',t{1},t{2});
                end
                
                if isempty(s3)
                    txt = {s0,s1,s2};
                else
                    txt = {s0,s1,s2,s3};
                end
            else
                if isempty(s3)
                    txt = {s1,s2};
                else
                    txt = {s1,s2,s3};
                end
            end
            
            % Show text in readout:
            s.hReadout.Text = txt;
        end
        
        function updateWidgets(s)
            % Update span plot dynamically after it has been created.
            %
            % - Updates angle of endpoints
            % - Makes it invisible if either endpoint is missing
            % - Updates color to reflect dataset indices
            
            c = [s.SpanCplx{:}]; % could be 0, 1 or 2 elements
            if numel(c) < 2
                % Make invisible
                s.hSpan.Visible = 'off';
                s.hReadout.Visible = 'off';
            else
                % Make visible, update sector coords, color
                
                if strcmpi(s.hSpan.Visible,'off')
                    % Reset last angle span, to enforce <= 180 degree span
                    % reordering when a span becomes newly visible:
                    s.pLastSpanCCW = 0;
                end
                
                if s.pEndpointsWereChanged
                    % For a change in one of the two span endpoints:
                    %
                    % No adaptive behavior (i.e., beyond 360 we decrease
                    % slowly to 0 then back again -- vs one-shot change)
                    isFullCircle = false;
                    updateLastSpanCCW(s,c);
                    
                    if ~s.PreserveReflexAngle && (s.pLastSpanCCW > pi)
                        % angle diff is > 180, make it <= 180
                        c1 = c(1);
                        c2 = c(2);
                        c = [c2 c1];
                        s.SpanCplx = {c2 c1};
                        s.SpanIDs = s.SpanIDs([2 1]);
                        updateLastSpanCCW(s,c);
                    end
                else
                    % For a change in the markers connected to the span:
                    %
                    % Adaptive behavior that allows wrapping past 360 and
                    % then decreasing slowly, etc.
                    [c,isFullCircle] = applyAdaptiveReordering(s,c);
                end
                
                ang_c = angle(c);
                if isFullCircle
                    % Explicitly set 2nd point to 360 past 1st point
                    ang_c(2) = ang_c(1)+2*pi;
                end
                
                % Use normalized coords for mag and theta
                %   (0-deg=right, always CCW, r is 0 to 1)
                [verts,faces] = internal.polariCommon.sectorsPatchRounded( ...
                    1.0,ang_c,s.ZPlane);
                
                set(s.hSpan, ...
                    'Faces',faces, ...
                    'Vertices',verts, ...
                    'Visible','on');
                
                updateSpanFaceColor(s);
                %alignWithPolarPlot(s);
                
                if s.Visible
                    updateReadout(s,isFullCircle);
                end
            end
        end
        
        function updateLastSpanCCW(s,c)
            % Record CCW angle difference, in radians, from c1 to c2
            s.pLastSpanCCW = internal.polariCommon.cangleDiff(c(1),c(2));
        end
        
        function [c,isFullCircle] = applyAdaptiveReordering(s,c)
            % If PreserveReflexAngle, reorder points if new angle span
            % exceeds 180 degrees and the last angle span was <= 90
            % degrees.
            %
            % If ~PreserveReflexAngle, no swapping occurs.
            %
            % For the initial angle span, the last angle span may be reset
            % to the same value as the new angle span, such that no
            % reordering takes place.  In this way, initial angles > 180
            % degrees can be achieved, say, from the command line.
            
            c1 = c(1);
            c2 = c(2);
            thisSpanCCW = internal.polariCommon.cangleDiff(c1,c2);
            
            if s.ResetAfter360Span
                % Adapt direction so initial span is < 180.
                % When span increases past 360 deg, reset span to 0.
                swap = (thisSpanCCW > pi) ...
                    && ~s.PreserveReflexAngle ...
                    && (s.pLastSpanCCW <= pi/2);
            else
                % Adapt direction so initial span is < 180.
                % When span increases past 360 deg, decrease span.
                swap = false;
                if ~s.PreserveReflexAngle
                    if (thisSpanCCW <= pi/2) && (s.pLastSpanCCW >= 3/2*pi);
                        % Decrease after 360
                        swap = true;
                        thisSpanCCW = 2*pi - thisSpanCCW;
                        
                    elseif (thisSpanCCW > pi) && (s.pLastSpanCCW <= pi/2)
                        % Disallow > 180 deg
                        swap = true;
                        thisSpanCCW = 0;
                    end
                end
            end
            
            if swap
                % New angle span > 180 degrees.
                % Last angle span <= 90 degrees.
                % Swap endpoints to bring new angle span <= 180.
                c = [c2 c1];
                s.SpanCplx = {c2 c1};
                s.SpanIDs = s.SpanIDs([2 1]);
            end
            s.pLastSpanCCW = thisSpanCCW;
            isFullCircle = thisSpanCCW==2*pi;
        end
        
        function ds_idx = getDatasetIndexForMarkers(s)
            % Return two-element vector of dataset indices,corresponding to
            % start and end marker in that order.
            %
            % If an endpoint isn't associated with a dataset, 0 is returned
            % as the index for that endpoint.
            
            p = s.PolariObj;
            
            id1 = s.SpanIDs{1};
            if ~isempty(id1)
                idx1 = getDataSetIndex(findAngleMarkerByID(p,id1));
            else
                idx1 = 0;
            end
            
            id2 = s.SpanIDs{2};
            if ~isempty(id2)
                idx2 = getDataSetIndex(findAngleMarkerByID(p,id2));
            else
                idx2 = 0;
            end
            
            ds_idx = [idx1 idx2];
        end
        
        function updateSpanFaceColor(s,ds_idx)
            % Update face color of span to reflect marker datasets.
            
            if nargin < 2
                ds_idx = getDatasetIndexForMarkers(s); % 2-element vector
            end
            
            % Faster exit if no change
            %if isequal(ds_idx, s.pSpanFaceColorDatasetIndices)
                %return
            %end
            p = s.PolariObj;
            if any(ds_idx==0) || ds_idx(1)~=ds_idx(2)
                % Differing datasets - use the "hybrid" span color
                %
                % Use a color derived from a lighter shade of
                % GridForegroundColor:
                faceC = internal.ColorConversion.brighten( ...
                     p.GridForegroundColor, 0.4);
                %faceC = [1 1 1]*.8;
            else
                % Same datasets - use a slightly darker "dataset glow
                % color".
                %
                %h = getDataWidgetHandles(s.PolariObj);
                %c = h(ds_idx(1)).Color;
                %
                %c = s.PolariObj.hDataLine(ds_idx(1)).Color;
                %
                c = getDatasetColor(s.PolariObj,ds_idx(1));
                faceC = internal.ColorConversion.glowColor(c,0.6);
            end
            span = s.hSpan;
            assert(~isempty(span));
            
            if s.Fill && ~isIntensityData(p)
                span.FaceColor = faceC; % update span polygon color
            else
                % Outline-only for span visualization:
                span.FaceColor = 'none';
                %span.EdgeColor = faceC;
                span.EdgeColor = 'k';
            end
            
            % Update readout background color
            %
            % Brighten the face color to better match the span color.
            % The span is actually transparent (SpanAlpha), in order
            % for the grid to show through.  But text doesn't support
            % alpha, so alpha=1 implicitly.  Thus its color is too
            % saturated relative to the transparent span.  So we
            % brighten it a bit.
            s.hReadout.NormalBackgroundColor = ...
                internal.ColorConversion.brighten(faceC, 1-s.SpanAlpha);
            
            % Cache for next time
            %s.pSpanFaceColorDatasetIndices = ds_idx;
        end
        
        function deleteWidgets(s)
            % All graphical widgets are parented to hLocalXform
            delete(s.hLocalXform);
            s.hLocalXform = [];
            s.hSpan = [];
        end
        
        function createWidgets(s)
            p = s.PolariObj;
            
            tagStr = sprintf('%s%d',mfilename,p.pAxesIndex);
            xfrm = hgtransform( ...
                'Parent',p.hAxes, ...
                'Tag',tagStr);
            s.hLocalXform = xfrm;

            % Disable data cursor from attaching to this
            b = hggetbehavior(xfrm,'DataCursor');
            b.Enable = false;
            b = hggetbehavior(xfrm,'Plotedit');
            b.Enable = false;
            
            % Create context menu
            figParent = ancestor(p.hAxes,'figure');
            
            hc = uicontextmenu( ...
                'Parent',figParent, ...
                'Callback',@(h,ev)updateSpanContextMenu(s,h), ...
                'HandleVisibility', 'off');
            
            faceC = 'none';
            edgeC = 'none';
            
            % Initialize cache, used for faster updates
            %s.pSpanFaceColorDatasetIndices = [0 0];
            
            ht = patch( ...
                'Parent',xfrm, ...
                'Tag',tagStr, ...
                'XData',[], ...
                'YData',[], ...
                'ZData',[], ...
                'FaceColor', faceC, ...
                'FaceAlpha', s.SpanAlpha, ...
                'EdgeColor', edgeC, ...
                'Marker', 'none', ...
                'LineStyle', '-', ...
                'LineWidth', 1.5, ...
                'Visible','on');
            s.hSpan = ht;
            set(ht,'uicontextmenu',hc);
        end
    end
end
