function airplane_area_gui()

   opengl software;  % Force software rendering to avoid OpenGL hardware lock issues

    f = figure('Name', 'Airplane Area Estimator', 'NumberTitle', 'off', ...
           'MenuBar', 'none', ...      % <- disables menu bar
           'ToolBar', 'figure', ...      % <- disables standard toolbar
               'Position', [100 100 1000 600], 'Resize', 'on', ...
               'Color', [0.94 0.94 0.94]);

    % set(f, 'Pointer', 'arrow');

    ax = axes('Parent', f, 'Units', 'normalized', 'Position', [0.20 0.05 0.55 0.9]);
    axis(ax, 'off');  % Hide ticks and labels
    helpAx = [];       % help overlay axes
    helpOverlay = [];  % image object for help image
    helpAx = axes('Parent', f, ...
              'Units', 'normalized', ...
              'Position', [0.20 0.05 0.55 0.9], ...
              'Visible', 'off', ...
              'Color', 'none');  % Transparent background
    axis(helpAx, 'off');  % Hide ticks and labels

    resultBox = uicontrol(f, 'Style', 'edit', ...
              'Max', 10, 'Min', 0, ...  % multi-line edit
              'Units', 'normalized', 'Position', [0.78 0.15 0.2 0.8], ...
              'HorizontalAlignment', 'left', ...
              'FontName', 'monospaced');
    uicontrol(f, 'Style', 'text', 'String', 'Wingspan:', ...
              'Units', 'normalized', 'Position', [0.01 0.85 0.10 0.04]);
    wingspanBox = uicontrol(f, 'Style', 'edit', ...
              'Units', 'normalized', 'Position', [0.11 0.85 0.08 0.04]);

    uicontrol(f, 'Style', 'text', 'String', 'Units:', ...
              'Units', 'normalized', 'Position', [0.01 0.80 0.08 0.03]);
    unitDropdown = uicontrol(f, 'Style', 'popupmenu', ...
              'String', {'ft', 'm', 'in'}, ...
              'Units', 'normalized', 'Position', [0.11 0.80 0.08 0.035]);

    uicontrol(f, 'Style', 'pushbutton', 'String', 'Load Image', 'Units', 'normalized', 'Position', [0.01 0.70 0.18 0.045], 'Callback', {@loadImage});
    uicontrol(f, 'Style', 'pushbutton', 'String', 'Set Scale (Wingtip)', ...
              'Units', 'normalized', 'Position', [0.01 0.64 0.18 0.045], 'Callback', {@scaleImage});
    uicontrol(f, 'Style', 'pushbutton', 'String', 'Measure Area', ...
              'Units', 'normalized', 'Position', [0.01 0.52 0.18 0.045], 'Callback', {@measureArea});
    uicontrol(f, 'Style', 'pushbutton', 'String', 'Measure Distance', ...
              'Units', 'normalized', 'Position', [0.01 0.58 0.18 0.045], 'Callback', {@measureDistance});
    uicontrol(f, 'Style', 'pushbutton', 'String', 'Draw Line', ...
              'Units', 'normalized', 'Position', [0.01 0.46 0.18 0.045], 'Callback', {@drawLine});
    uicontrol(f, 'Style', 'pushbutton', 'String', 'Add Label', ...
              'Units', 'normalized', 'Position', [0.01 0.40 0.18 0.045], 'Callback', {@addLabel});
    uicontrol(f, 'Style', 'pushbutton', 'String', 'Undo', ...
              'Units', 'normalized', 'Position', [0.01 0.34 0.18 0.045], 'Callback', {@undoLast});
    uicontrol(f, 'Style', 'pushbutton', 'String', 'Clear All', ...
              'Units', 'normalized', 'Position', [0.01 0.28 0.18 0.045], 'Callback', {@clearAll});
    uicontrol(f, 'Style', 'pushbutton', 'String', 'Save Annotated Image', ...
              'Units', 'normalized', 'Position', [0.01 0.22 0.18 0.045], 'Callback', {@saveAnnotatedImage});
    uicontrol(f, 'Style', 'pushbutton', 'String', 'Export Session', ...
              'Units', 'normalized', 'Position', [0.01 0.16 0.18 0.045], 'Callback', {@exportSession});
    uicontrol(f, 'Style', 'pushbutton', 'String', 'Load Session', ...
              'Units', 'normalized', 'Position', [0.01 0.10 0.18 0.045], 'Callback', {@loadSession});
    uicontrol(f, 'Style', 'pushbutton', 'String', 'Longitudinal Help', ...
          'Units', 'normalized', 'Position', [0.78 0.10 0.20 0.045], ...
          'Callback', @(~,~) showHelpImage('LongitudinalMeasurements.png'));    
    uicontrol(f, 'Style', 'pushbutton', 'String', 'Lat-Dir Help', ...
          'Units', 'normalized', 'Position', [0.78 0.05 0.20 0.045], ...
          'Callback', @(~,~) showHelpImage('LatDirMeasurements.png'));
    
    uicontrol(f, 'Style', 'pushbutton', 'String', 'Dismiss Help', ...
          'Units', 'normalized', 'Position', [0.78 0.00 0.20 0.045], ...
          'Callback', @(~,~) dismissHelpImage());
% Overlay image handle (used only for help)
    helpOverlay = [];
    axis(ax, 'image');   % Maintains aspect ratio
function showHelpImage(filename)
    % Check if help image exists
    if ~exist(filename, 'file')
        errordlg(['Help image not found: ' filename]); 
        return;
    end

    % Read the help image
    imgHelp = imread(filename);

    % Clear previous help overlay image if it exists
    if isgraphics(helpOverlay)
        delete(helpOverlay);
    end

    % Clear the overlay axes and display the new help image
    cla(helpAx);  % Clear previous content
    helpOverlay = imshow(imgHelp, 'Parent', helpAx);
    uistack(helpAx, 'top');     % Bring help overlay to top

    % Ensure the help axes are visible but passive (no ticks, no interactivity)
    set(helpAx, ...
        'Visible', 'on', ...
        'XTick', [], 'YTick', [], ...
        'XColor', 'none', 'YColor', 'none', ...
        'HitTest', 'off', 'PickableParts', 'none');  % make it passive

    axis(helpAx, 'off');  % Additional safeguard

    % Disable zoom, pan, datacursor, rotate, etc.
    disableDefaultInteractivity(helpAx);

    % Hide the floating axes toolbar in the top-right corner (if present)
    if isprop(helpAx, 'Toolbar') && ~isempty(helpAx.Toolbar)
        helpAx.Toolbar.Visible = 'off';
    end
end

function dismissHelpImage()
    if isgraphics(helpOverlay)
        delete(helpOverlay);
        helpOverlay = [];
    end
    cla(helpAx);
    set(helpAx, ...
        'Visible', 'off', ...
        'HitTest', 'off', ...
        'PickableParts', 'none');
    
    uistack(ax, 'top');  % Restore drawing axes to top
end

    img = []; imgPath = ''; scale = 1;
    imgData = struct('image', [], 'scale', 1);
    roiHandles = {}; drawHandles = {}; scaleLine = [];

    % zoom(f, 'on'); pan(f, 'on');

    hZoom = zoom(f);
    hPan = pan(f);
    setAllowAxesZoom(hZoom, ax, true);
    setAllowAxesPan(hPan, ax, true);

    uicontrol(f, 'Style', 'pushbutton', 'String', 'Measure Angle', ...
              'Units', 'normalized', 'Position', [0.01 0.04 0.18 0.045], 'Callback', {@measureAngle});

    function scaleImage(~, ~)
        unitStr = unitDropdown.String{unitDropdown.Value};
        if isempty(img)
            errordlg('Please load an image first.'); return;
        end
        title(ax, 'Click on both wingtips');
        [x, y] = ginput(2);
        pixel_span = norm([x(2)-x(1), y(2)-y(1)]);
        wingspan = str2double(strtrim(wingspanBox.String));
        if isnan(wingspan) || wingspan <= 0
            errordlg('Invalid wingspan entered.'); return;
        end
        scale = wingspan / pixel_span;
        imgData.scale = scale;
        imgData.unit = unitStr;
        title(ax, 'Scaling complete');

        if isgraphics(scaleLine), delete(scaleLine); end
        hold(ax, 'on');
        scaleLine = plot(ax, x, y, 'r--', 'LineWidth', 1.5);
        midX = mean(x); midY = mean(y);
        text(ax, midX, midY, sprintf('%.2f %s', wingspan,  unitStr), 'Color', 'red', 'FontSize', 10, 'BackgroundColor', 'w', 'EdgeColor', 'black', 'Margin', 2);
    end

    function measureArea(~, ~)
        if isempty(imgData.image) || imgData.scale <= 0
            errordlg('Load image and set scale first.'); return;
        end
        part = questdlg('Select part to measure:', 'Area Measurement', 'Wing', 'Horizontal Tail', 'More Options', 'Wing');
        if isempty(part), return; end
        if strcmp(part, 'More Options')
            part = questdlg('Select additional part:', 'Extended Measurement', 'Fuselage Side Area', 'Fuselage Top Area', 'Cancel', 'Fuselage Side Area');
            if isempty(part) || strcmp(part, 'Cancel'), return; end
        end
        title(ax, ['Draw polygon for ', part]);
        roi = drawpolygon('Parent', ax, 'FaceAlpha', 0.2, 'Color', 'cyan');
        roiHandles{end+1} = roi;
        mask = createMask(roi);
        area = sum(mask(:)) * imgData.scale^2;
        current = resultBox.String;
        if ischar(current) || isstring(current), current = cellstr(current); end
        unitStr = imgData.unit;
        current{end+1} = sprintf('%s: %.2f %s^2', part, area, unitStr);
        resultBox.String = current;
    end

    function measureDistance(~, ~)
        if isempty(imgData.image) || imgData.scale <= 0
            errordlg('Load image and define scale first.'); return;
        end
        title(ax, 'Click two points to measure distance');
        [x, y] = ginput(2);
        dist_px = norm([x(2)-x(1), y(2)-y(1)]);
        dist = dist_px * imgData.scale;
        current = resultBox.String;
        if ischar(current) || isstring(current), current = cellstr(current); end
    	unitStr = imgData.unit;
        current{end+1} = sprintf('Distance: %.2f %s', dist, unitStr);
        resultBox.String = current;
    end

    function drawLine(~, ~)
        if isempty(imgData.image)
            errordlg('Please load image first.'); return;
        end
        title(ax, 'Click and drag to draw line');
        roi = drawline('Parent', ax, 'Color', 'b', 'LineWidth', 2);
        addlistener(roi, 'ROIMoved', @(src, evt) updateLineColor(src));
        addlistener(roi, 'DrawingFinished', @(src, evt) updateLineColor(src));
        updateLineColor(roi);
        roi.addlistener('ROIMoved', @(~,~) updateLineColor(roi));
        roi.InteractionsAllowed = 'all';
        addDeleteMenu(roi);
        drawHandles{end+1} = roi;
    end

    function addLabel(~, ~)
        if isempty(imgData.image)
            errordlg('Please load image first.'); return;
        end
        title(ax, 'Click location to add label');
        [x, y] = ginput(1);
        txt = inputdlg('Enter label text:', 'Label Input');
        if isempty(txt), return; end
        h = text(ax, x, y, txt{1}, 'Color', 'blue', 'FontSize', 10, 'BackgroundColor', 'w', 'Margin', 2, 'EdgeColor', 'black', 'ButtonDownFcn', @(src,~) startDrag(src));
        addDeleteMenu(h);
        drawHandles{end+1} = h;
        current = resultBox.String;
        if ischar(current) || isstring(current), current = cellstr(current); end
        current{end+1} = ['Label: ', txt{1}];
        resultBox.String = current;
    end

    function undoLast(~, ~)
        if ~isempty(roiHandles)
            h = roiHandles{end};
            if isvalid(h), delete(h); end
            roiHandles(end) = [];
        elseif ~isempty(drawHandles)
            h = drawHandles{end};
            if isvalid(h), delete(h); end
            drawHandles(end) = [];
        end
    end

    function clearAll(~, ~)
        deleteAllROIs();
        deleteAllDrawings();
        if isgraphics(scaleLine), delete(scaleLine); end
        scaleLine = [];
        resultBox.String = {};
        cla(ax);
        if ~isempty(imgData.image)
            imshow(imgData.image, 'Parent', ax);
        end
    end

    function deleteAllROIs()
        for i = 1:length(roiHandles)
            if isvalid(roiHandles{i}), delete(roiHandles{i}); end
        end
        roiHandles = {};
    end

    function deleteAllDrawings()
        for i = 1:length(drawHandles)
            if isvalid(drawHandles{i}), delete(drawHandles{i}); end
        end
        drawHandles = {};
    end

    function addDeleteMenu(h)
        cmenu = uicontextmenu(f);
        uimenu(cmenu, 'Label', 'Delete', 'Callback', @(~,~) deleteGraphic(h));
        h.UIContextMenu = cmenu;
    end

    function deleteGraphic(h)
        if isvalid(h), delete(h); end
        drawHandles(cellfun(@(x) ~isvalid(x), drawHandles)) = [];
    end

    function startDrag(hObj)
        if ~isvalid(hObj), return; end
        set(f, 'WindowButtonMotionFcn', {@dragging, hObj});
        set(f, 'WindowButtonUpFcn', @stopDragging);
    end

    function dragging(~, ~, hObj)
        cp = get(ax, 'CurrentPoint');
        hObj.Position = [cp(1,1), cp(1,2)];
    end

    function stopDragging(~, ~)
        set(f, 'WindowButtonMotionFcn', '');
        set(f, 'WindowButtonUpFcn', '');
    end

function measureAngle(~, ~)
    if length(drawHandles) < 2
        errordlg('Draw at least two lines to measure relative angle.'); return;
    end
    title(ax, 'Click near first line');
    [x1, y1] = ginput(1);
    h1 = findClosestLine(x1, y1);
    if isempty(h1), errordlg('No valid line found near first click.'); return; end

    title(ax, 'Click near second line');
    [x2, y2] = ginput(1);
    h2 = findClosestLine(x2, y2);
    if isempty(h2), errordlg('No valid line found near second click.'); return; end

    v1 = diff(h1.Position);
    v2 = diff(h2.Position);
    h1 = findClosestLine(x1, y1);

    angle_deg = acosd(dot(v1, v2) / (norm(v1) * norm(v2)));

    current = resultBox.String;
    if ischar(current) || isstring(current), current = cellstr(current); end
    current{end+1} = sprintf('Relative Angle: %.2f degrees', angle_deg);
    resultBox.String = current;

    p1 = mean(h1.Position);
    p2 = mean(h2.Position);
    pmid = (p1 + p2) / 2;
    hold(ax, 'on');
    txt = text(ax, pmid(1), pmid(2), sprintf('%.1f°', angle_deg), 'Color', 'magenta', 'FontSize', 12, 'FontWeight', 'bold', 'BackgroundColor', 'w');
   % set(f, 'Pointer', 'arrow');
    drawHandles{end+1} = txt;
    addDeleteMenu(txt);
end
    
function h = findClosestLine(x, y)
        minDist = inf;
        h = [];
        pt = [x, y];
        for i = 1:length(drawHandles)
            candidate = drawHandles{i};
            if isvalid(candidate) && isa(candidate, 'images.roi.Line')
                pos = candidate.Position;
                d = pointToLineDistance(pt, pos(1,:), pos(2,:));
                if d < minDist && d < 10 % 10-pixel click tolerance
                    minDist = d;
                    h = candidate;
                end
            end
        end
    end

    function d = pointToLineDistance(p, a, b)
        % Compute perpendicular distance from point p to line ab
        ab = b - a;
        ap = p - a;
        t = dot(ap, ab) / dot(ab, ab);
        t = max(0, min(1, t));
        proj = a + t * ab;
        d = norm(p - proj);
    end
    function exportSession(~, ~)
        [file, path] = uiputfile('*.mat', 'Save Session As');
        if isequal(file, 0), return; end
        polygons = cellfun(@(r) r.Position, roiHandles, 'UniformOutput', false);
        lines = {}; labels = {};
        for i = 1:length(drawHandles)
            h = drawHandles{i};
            if isvalid(h)
                if isa(h, 'images.roi.Line')
                    lines{end+1} = h.Position;
                elseif isa(h, 'matlab.graphics.primitive.Text')
                    labels{end+1} = struct('Position', h.Position, 'String', h.String);
                end
            end
        end
        resultText = resultBox.String;
        unitStr = unitDropdown.String{unitDropdown.Value};
        save(fullfile(path, file), 'imgPath', 'scale', 'polygons', 'lines', 'labels', 'resultText', 'unitStr');
        msgbox('Session exported successfully.', 'Success');
    end

    function loadSession(~, ~)
        % imshow(img, 'Parent', ax);
        uistack(ax, 'top');
        [file, path] = uigetfile('*.mat', 'Load Session');
        if isequal(file, 0), return; end
        load(fullfile(path, file), 'imgPath', 'scale', 'polygons', 'lines', 'labels', 'resultText', 'unitStr');
        % load(fullfile(path, file), ... , 'unitStr');
        unitIdx = find(strcmp(unitDropdown.String, unitStr));
        if ~isempty(unitIdx)
            unitDropdown.Value = unitIdx;
        end
        imgData.unit = unitStr;
        if exist(imgPath, 'file')
            img = imread(imgPath);
            imshow(img, 'Parent', ax);
            uistack(ax, 'top');  % Ensure ax is topmost
            imgData.image = img;
            imgData.scale = scale;
        else
            errordlg('Original image file not found.'); return;
        end
        deleteAllROIs(); deleteAllDrawings();
                imshow(img, 'Parent', ax);

        for i = 1:length(polygons)
            roi = drawpolygon('Parent', ax, 'Position', polygons{i}, 'FaceAlpha', 0.2, 'Color', 'cyan');
            roiHandles{end+1} = roi;
        end
        for i = 1:length(lines)
            line = drawline('Parent', ax, 'Position', lines{i}, 'Color', 'b', 'LineWidth', 2);
            drawHandles{end+1} = line;
            addDeleteMenu(line);
        end
        for i = 1:length(labels)
            l = labels{i};
            h = text(ax, l.Position(1), l.Position(2), l.String, 'Color', 'blue', 'FontSize', 10, 'BackgroundColor', 'w', 'Margin', 2, 'EdgeColor', 'black', 'ButtonDownFcn', @(src,~) startDrag(src));
            drawHandles{end+1} = h;
            addDeleteMenu(h);
        end
        resultBox.String = resultText;
        wingspanBox.String = '';
    end

    function updateLineColor(roi)
        if ~isvalid(roi), return; end
        pos = roi.Position;
        if size(pos, 1) ~= 2, return; end
        dx = pos(2,1) - pos(1,1);
        dy = pos(2,2) - pos(1,2);
        tol = 1e-2; % tolerance for horizontal/vertical
        if abs(dx) < tol || abs(dy) < tol
            roi.Color = 'r';
        else
            roi.Color = 'b';
        end
    end
        
    % removed duplicate end
    function saveAnnotatedImage(~, ~)
        if isempty(imgData.image)
            errordlg('No image loaded to save.'); return;
        end
        [file, path] = uiputfile({'*.png'; '*.jpg'; '*.bmp'}, 'Save Annotated Image As');
        if isequal(file, 0), return; end
        [imgH, imgW, ~] = size(imgData.image);
        tempFig = figure('Visible', 'off', 'Position', [100 100 imgW imgH]);
        set(f, 'Pointer', 'arrow');
        tempAx = axes('Parent', tempFig, 'Position', [0 0 1 1]);
        imshow(imgData.image, 'Parent', tempAx); axis(tempAx, 'off');
        for i = 1:length(roiHandles)
            if isvalid(roiHandles{i})
                drawpolygon(tempAx, 'Position', roiHandles{i}.Position, 'FaceAlpha', 0.2, 'Color', 'cyan');
            end
        end
        for i = 1:length(drawHandles)
            h = drawHandles{i};
            if isvalid(h)
                if isa(h, 'images.roi.Line')
                    drawline(tempAx, 'Position', h.Position, 'Color', 'b', 'LineWidth', 2);
                elseif isa(h, 'matlab.graphics.primitive.Text')
                    text(tempAx, h.Position(1), h.Position(2), h.String, 'Color', h.Color, 'FontSize', h.FontSize, 'BackgroundColor', h.BackgroundColor, 'Margin', h.Margin, 'EdgeColor', h.EdgeColor);
                end
            end
        end
        exportgraphics(tempAx, fullfile(path, file), 'Resolution', 300);
        close(tempFig);
        msgbox('High-resolution annotated image saved.', 'Success');
    end

function loadImage(~, ~)
    imshow(img, 'Parent', ax);
    uistack(ax, 'top');
    [file, path] = uigetfile({'*.png;*.jpg;*.jpeg;*.bmp;*.pdf'}, 'Select Image or PDF');
    if isequal(file, 0), return; end
    [~, name, ext] = fileparts(file);
    fullpath = fullfile(path, file);

    if strcmpi(ext, '.pdf')
        % Prompt user for page number
        answer = inputdlg('Enter PDF page number to load:', 'Select PDF Page', [1 40], {'1'});
        if isempty(answer), return; end
        pageNum = str2double(answer{1});
        if isnan(pageNum) || pageNum <= 0
            errordlg('Invalid page number'); return;
        end

        % Try default Ghostscript path
        defaultGSPath = '"C:\Program Files\gs\gs10.05.1\bin\gswin64c.exe"';
        [status, ~] = system([defaultGSPath ' -v']);

        % If not found, prompt user to locate Ghostscript manually
        if status ~= 0
            uiwait(msgbox(['Ghostscript not found at default path: ' defaultGSPath newline newline ...
                'You will now be prompted to locate gswin64c.exe manually.' newline ...
                'If you haven''t installed Ghostscript yet, download it from:' newline ...
                'https://www.ghostscript.com/download/gsdnld.html'], ...
                'Ghostscript Not Found', 'modal'));
            [gsFile, gsPathManual] = uigetfile('gswin64c.exe', 'Select Ghostscript Executable (gswin64c.exe)');
            if isequal(gsFile, 0)
                errordlg('Ghostscript is required to load PDF pages.'); return;
            end
            gsPath = ['"' fullfile(gsPathManual, gsFile) '"'];
        else
            gsPath = defaultGSPath;
        end

        % Convert specified PDF page to PNG
        outputImage = fullfile(path, sprintf('%s_page%d.png', name, pageNum));
        cmd = sprintf('%s -dNOPAUSE -dBATCH -sDEVICE=pngalpha -r300 -dFirstPage=%d -dLastPage=%d -sOutputFile="%s" "%s"', ...
                      gsPath, pageNum, pageNum, outputImage, fullpath);
        status = system(cmd);
        if status ~= 0 || ~exist(outputImage, 'file')
            errordlg('Ghostscript failed to convert the PDF.'); return;
        end

        imgPath = outputImage;
    else
        imgPath = fullfile(path, file);
    end

    % Load image to GUI
    img = imread(imgPath);
    imshow(img, 'Parent', ax);
    uistack(ax, 'top');  % Ensure ax is topmost
    imgData.image = img;
    imgData.scale = 1;
    scale = 1;
    deleteAllROIs(); deleteAllDrawings(); resultBox.String = {};
    if isgraphics(scaleLine), delete(scaleLine); end
    scaleLine = [];
end
end


