function addCustomTerrain(terrainName, varargin)
%addCustomTerrain Add use of custom terrain data
%   addCustomTerrain(NAME,FILES) adds the terrain data specified by NAME
%   and FILES. The string scalar or character vector NAME is a user-defined
%   identifier for the terrain data, and FILES is a list of DTED files.
%   FILES is specified as a string vector, character vector, or cell array
%   of character vectors. Files must be in the current directory, in a
%   directory on the MATLAB path, or be specified using full or relative
%   path. If multiple files are specified, they must combine together to
%   define a complete rectangular geographic region, or else FillMissing
%   must be set to true.
%
%   addCustomTerrain(___,Name,Value) adds the terrain data with additional
%   options specified by one or more Name-Value pairs.
%
%   The custom terrain data added by this function is available in this
%   session of MATLAB and future sessions of MATLAB until
%   removeCustomTerrain is called.
%
%   addCustomTerrain Name-Value pairs:
%
%   Attribution - Attribution of custom terrain data, specified as a string
%      scalar or character vector. The attribution is displayed on the Site
%      Viewer map. The default value is an empty string.
%
%   FillMissing - Fill data for missing files with the value 0 (mean sea
%      level elevation), specified as true or false. Missing files are
%      those required to complete a rectangular geographic region with the
%      input FILES. The default value is false.
%
%   WriteLocation - The folder name to write extracted terrain files to,
%      specified as a string scalar or character vector. The specified
%      folder must exist and have write permissions. The folder location is
%      persistent across MATLAB sessions. The default value is a folder
%      name generated using tempname.
%
%   % Example 1: Import DTED data and use in Site Viewer
%   % (Requires Antenna Toolbox(TM) or Communications Toolbox(TM))
%
%   % Add terrain for a region around Boulder, CO. The DTED file was 
%   % downloaded from the "SRTM Void Filled" data set available from the 
%   % U.S. Geological Survey.
%   dtedfile = "n39_w106_3arc_v2.dt1";
%   attribution = "SRTM 3 arc-second resolution. Data available " + ...
%      "from the U.S. Geological Survey.";
%   addCustomTerrain("southboulder",dtedfile,"Attribution",attribution)
%
%   % Specify custom terrain data with Site Viewer using the name above
%   % and show South Boulder Peak.
%   viewer = siteviewer("Terrain","southboulder");
%   southboulderpeak = txsite("Name","South Boulder Peak", ...
%      "Latitude",39.95384, ...
%      "Longitude",-105.29916);
%   show(southboulderpeak)
%
%   % Remove custom terrain data.
%   close(viewer)
%   removeCustomTerrain("southboulder")
%
%   % Example 2: Import DTED data and use in geoglobe
%   % (Requires Mapping Toolbox(TM))
%
%   % Add terrain for a region around Boulder, CO. The DTED file was 
%   % downloaded from the "SRTM Void Filled" data set available from the 
%   % U.S. Geological Survey.
%   dtedfile = "n39_w106_3arc_v2.dt1";
%   attribution = "SRTM 3 arc-second resolution. Data available " + ...
%      "from the U.S. Geological Survey.";
%   addCustomTerrain("southboulder",dtedfile,"Attribution",attribution)
%
%   % Specify custom terrain data with geoglobe using the name above
%   % and plot a line from an observer 10m above South Boulder Peak to the
%   % surface of nearby Gross Reservoir.
%   fig = uifigure;
%   g = geoglobe(fig,"Terrain","southboulder");
%   lat = [39.95384 39.95];
%   lon = [-105.29916 -105.3608];
%   h = [10 0];
%   hold(g,"on")
%   geoplot3(g,lat,lon,h,"y","HeightReference","terrain","LineWidth",3)
%
%   % Close figure before removing custom terrain data.
%   close(fig)
%
%   % Remove custom terrain data.
%   removeCustomTerrain("southboulder")
%
%   See also geoglobe, geoplot3, siteviewer, removeCustomTerrain

% Copyright 2018-2023 The MathWorks, Inc.

narginchk(2,inf)

% Validate and get parameters
[terrainData, p] = terrain.internal.Tiler.parseAddCustomTerrainInputs(varargin{:});
terrainName = validateTerrainName(terrainName);
terrainData = validateTerrainData(p, terrainData);
attribution = validateAttribution(p);
[writeLocation, writeLocationCleanup] = validateWriteLocation(p);
fillMissing = validateFillMissing(p);
[isDefaultMaxZoomLevel, maxZoomLevel] = validateMaxZoomLevel(p);
showWaitbar = validateShowWaitbar(p);
heightReference = validateHeightReference(p);
outputHeightReference = validateOutputHeightReference(p);

% Create terrain source, which also validates files
try
    terrainSource = terrain.internal.TerrainSource.createFromData(terrainName, terrainData, ...
        'FillMissing', fillMissing, ...
        'HeightReference', heightReference);
catch e
    throw(e)
end

% Create terrain tiler
if isDefaultMaxZoomLevel
    maxZoomLevel = terrainSource.MaxZoomLevel;
end
tiler = terrain.internal.Tiler(terrainSource, writeLocation, ...
    'MaxZoomLevel', maxZoomLevel, ...
    'ShowWaitbar', showWaitbar, ...
    'OutputHeightReference', outputHeightReference);

% Generate heightmap terrain tiles
success = tiler.tile;

% Add settings group with personal value, which persist for user
if success
    s = settings;
    terrainSettings = s.shared.terrain;    
    g = terrainSettings.addGroup(terrainName);
    g.addSetting('Location', 'PersonalValue', writeLocation);
    g.addSetting('IsURLLocation', 'PersonalValue', false);
    g.addSetting('MaxZoomLevel', 'PersonalValue', maxZoomLevel);
    g.addSetting('Attribution', 'PersonalValue', attribution);
    g.addSetting('IntrinsicResolution', 'PersonalValue', terrainSource.IntrinsicResolution);
    g.addSetting('LatitudeLimits', 'PersonalValue', terrainSource.LatitudeLimits);
    g.addSetting('LongitudeLimits', 'PersonalValue', terrainSource.LongitudeLimits);
    g.addSetting('HeightReference', 'PersonalValue', tiler.OutputHeightReference);
    
    % Cancel deletion of write location, since normal execution has completed
    if ~isempty(writeLocationCleanup)
        cancel(writeLocationCleanup);
    end
end
end

function attribution = validateAttribution(p)
try
    attribution = p.Results.Attribution;
    validateattributes(attribution, {'char','string'}, {'scalartext'}, ...
        'addCustomTerrain', 'Attribution');
    attribution = char(attribution);
catch e
    throwAsCaller(e)
end
end

function [writeLocation, writeLocationCleanup] = validateWriteLocation(p)
try
    isDefaultWriteLocation = ismember('WriteLocation', p.UsingDefaults);
    if isDefaultWriteLocation
        % Create default location in temp dir
        writeLocation = tempname;
        mkdir(writeLocation);
        writeLocationCleanup = terrain.internal.onExit(@()rmdir(writeLocation,'s'));
    else
        writeLocation = p.Results.WriteLocation;
        writeLocationCleanup = [];
    end
    validateattributes(writeLocation, {'char','string'}, {'scalartext'}, ...
        'addCustomTerrain', 'WriteLocation');
    writeLocation = char(writeLocation);
    
    % Convert partial to full folder path
    if ~terrain.internal.TerrainSource.isfullpath(writeLocation)
        writeLocation = fullfile(pwd, writeLocation);
    end
    
    % Verify folder exists
    if ~exist(writeLocation,'dir')
        error(message('shared_terrain:terrain:WriteLocationNotFound'));
    end
    
    % Verify folder has write permission
    [status, values] = fileattrib(writeLocation);
    hasWritePermissions = status && values.UserWrite;
    if ~hasWritePermissions
        error(message('shared_terrain:terrain:WriteLocationPermissions'))
    end
catch e
    throwAsCaller(e)
end
end

function fillMissing = validateFillMissing(p)
try
    fillMissing = p.Results.FillMissing;
    validateattributes(fillMissing, {'logical'}, {'nonsparse','scalar'}, ...
        'addCustomTerrain', 'FillMissing');
catch e
    throwAsCaller(e)
end
end

function [isDefaultMaxZoomLevel, maxZoomLevel] = validateMaxZoomLevel(p)
try
    isDefaultMaxZoomLevel = ismember('MaxZoomLevel', p.UsingDefaults);
    maxZoomLevel = p.Results.MaxZoomLevel;
    if ~isDefaultMaxZoomLevel
        validateattributes(maxZoomLevel, {'numeric'}, {'nonnan','scalar','positive', ...
            'real','integer','<=',25}, 'addCustomTerrain', 'MaxZoomLevel');
        maxZoomLevel = double(maxZoomLevel);
    end
catch e
    throwAsCaller(e)
end
end

function showWaitbar = validateShowWaitbar(p)
try
    showWaitbar = p.Results.ShowWaitbar;
    validateattributes(showWaitbar, {'logical'}, {'nonsparse','scalar'}, ...
        'addCustomTerrain', 'ShowWaitbar');
catch e
    throwAsCaller(e)
end
end

function terrainName = validateTerrainName(terrainName)
try
    validateattributes(terrainName, {'char','string'}, {'scalartext'}, ...
        'addCustomTerrain','',1);
    terrainName = char(terrainName);
    
    % Name must be valid name
    if ~isvarname(terrainName)
        error(message('shared_terrain:terrain:InvalidTerrainName'));
    end
    
    % Name must be unique
    s = settings;
    terrainOptions = string(properties(s.shared.terrain));
    if ismember(lower(string(terrainName)), lower([terrainOptions; "none"]))
        error(message('shared_terrain:terrain:TerrainNameExists', terrainName));
    end
catch e
    throwAsCaller(e)
end
end

function terrainData = validateTerrainData(p, terrainData)
try
    if terrainData.IsFileSource
        files = terrainData.Source;
        if isempty(files) || ischar(files) || isscalar(files)
            validateattributes(files, {'char','string'}, {'scalartext'}, ...
                'addCustomTerrain', '', 2);
        else
            validateattributes(files, {'cell','string'}, {'vector'}, ...
                'addCustomTerrain', '', 2);
            if iscell(files)
                for k = 1:numel(files)
                    validateattributes(files{k}, {'char'}, {'scalartext'}, ...
                        'addCustomTerrain', '', 2);
                end
            end
        end
        files = string(files);
        terrain.internal.TerrainSource.validateTerrainFileFormat(p, files);
        terrainData.Source = files;
    end
catch e
    throwAsCaller(e)
end
end

function htref = validateHeightReference(p)
try
    htref = p.Results.HeightReference;
    htref = validatestring(htref, {'ellipsoid','geoid'}, ...
        'addCustomTerrain', 'HeightReference');
catch e
    throwAsCaller(e);
end
end

function outhtref = validateOutputHeightReference(p)
try
    outhtref = p.Results.OutputHeightReference;
    outhtref = validatestring(outhtref, {'ellipsoid','geoid'}, ...
        'addCustomTerrain', 'OutputHeightReference');
catch e
    throwAsCaller(e);
end
end