diff --git a/CMakeLists.txt b/CMakeLists.txt index 0bb8215..6040eee 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -65,6 +65,7 @@ set(SPLAT_SOURCES src/antenna_pattern.cpp src/boundary_file.cpp src/city_file.cpp + src/command_line_parser.cpp src/dem.cpp src/elevation_map.cpp src/gnuplot.cpp @@ -153,6 +154,7 @@ FetchContent_MakeAvailable(googletest) # Explicitly list test source files set(TEST_SOURCES tests/antenna_pattern_test.cpp + tests/command_line_parser_test.cpp tests/image_output_test.cpp tests/input_file_parsing_test.cpp tests/itwom_test.cpp @@ -172,6 +174,7 @@ set(TEST_REQUIRED_SOURCES src/antenna_pattern.cpp src/boundary_file.cpp src/city_file.cpp + src/command_line_parser.cpp src/dem.cpp src/elevation_map.cpp src/gnuplot.cpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index dbc47c3..32d927e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -11,6 +11,7 @@ add_executable(splat antenna_pattern.cpp boundary_file.cpp city_file.cpp + command_line_parser.cpp dem.cpp elevation_map.cpp gnuplot.cpp diff --git a/src/command_line_parser.cpp b/src/command_line_parser.cpp new file mode 100644 index 0000000..ff704e3 --- /dev/null +++ b/src/command_line_parser.cpp @@ -0,0 +1,540 @@ +/** @file command_line_parser.cpp + * + * Command-line argument parser for SPLAT! + * + * @copyright 1997 - 2018 John A. Magliacane (KD2BD) and contributors. + * See revision control history for contributions. + * This file is covered by the LICENSE.md file in the root of this project. + */ + +#include "command_line_parser.h" +#include "splat_run.h" +#include +#include + +void PrintHelp(const SplatRun &sr) { + std::cout + << "\n\t\t --==[ " << SplatRun::splat_name << " v" + << SplatRun::splat_version + << " Available Options... ]==--\n\n" + " -t txsite(s).qth\n" + " -r sr.rxsite.qth\n" + " -c plot LOS coverage of TX(s) with an RX antenna at X " + "feet/meters AGL\n" + " -L plot path loss map of TX based on an RX at X " + "feet/meters AGL\n" + " -s filename(s) of city/site file(s) to import (5 max)\n" + " -b filename(s) of cartographic boundary file(s) to " + "import (5 max)\n" + " -p filename of terrain profile graph to plot\n" + " -e filename of terrain elevation graph to plot\n" + " -h filename of terrain height graph to plot\n" + " -H filename of normalized terrain height graph to plot\n" + " -l filename of path loss graph to plot\n" + " -o filename of topographic map to generate (without " + "suffix)\n" + " -d sdf file directory path (overrides path in " + "~/.splat_path file)\n" + " -m earth radius multiplier\n" + " -n do not plot LOS paths in maps\n" + " -N do not produce unnecessary site or obstruction " + "reports\n" + " -f frequency for Fresnel zone calculation (MHz)\n" + " -R modify default range for -c or -L " + "(miles/kilometers)\n" + " -v N verbosity level. Default is 1. Set to 0 to quiet " + "everything.\n" + " -st use a single CPU thread (classic mode)\n" + " -hd Use High Definition mode (3600 ppd vs 1200 ppd). " + "Requires SRTM-1 SDF files.\n" + " -sc display smooth rather than quantized contour levels\n" + " -db threshold beyond which contours will not be " + "displayed\n" + " -nf do not plot Fresnel zones in height plots\n" + " -fz Fresnel zone clearance percentage (default = 60)\n" + " -gc ground clutter height (feet/meters)\n" + " -jpg when generating maps, create jpgs instead of pngs or " + "ppms\n" +#ifdef HAVE_LIBPNG + " -ppm when generating maps, create ppms instead of pngs or " + "jpgs\n" +#endif + " -tif create geotiff instead of png or jpeg\n" + " -ngs display greyscale topography as white in images\n" + " -erp override ERP in .lrp file (Watts)\n" + " -ano name of alphanumeric output file\n" + " -ani name of alphanumeric input file\n" + " -udt name of user defined terrain input file\n" + " -kml generate Google Earth (.kml) compatible output\n" + " -kmz generate Google Earth compressed (.kmz) output\n" + " -geo generate an Xastir .geo georeference file (with " + "image output)\n" + " -dbm plot signal power level contours rather than field " + "strength\n" + " -log copy command line std::string to this output file\n" + " -json create JSON file containing configuration \n" + " -gpsav preserve gnuplot temporary working files after " + "SPLAT! execution\n" + " -itwom invoke the ITWOM model instead of using " + "Longley-Rice\n" + " -imperial employ imperial rather than metric units for all " + "user I/O\n" + " -msl use MSL for TX/RX altitudes instead of AGL\n" + "-maxpages [" + << sr.maxpages + << "] Maximum Analysis Region capability: 1, 4, 9, 16, 25, 36, 49, " + "64 \n" + " -sdelim [" + << sr.sdf_delimiter + << "] Lat and lon delimeter in SDF filenames \n" + "\n" + "See the documentation for more details.\n\n"; +} + +bool ParseCommandLine(int argc, const char *argv[], SplatRun &sr, + CommandLineOptions &options) { + // Check for help or no arguments + if (argc == 1 || (argc == 2 && strcmp(argv[1], "--help") == 0)) { + options.show_help = true; + PrintHelp(sr); + return false; + } + + size_t x, y, z = 0; + + /* Scan for command line arguments */ + y = argc - 1; + + for (x = 1; x <= y; x++) { + if (strcmp(argv[x], "-R") == 0) { + z = x + 1; + + if (z <= y && argv[z][0] && argv[z][0] != '-') { + sscanf(argv[z], "%lf", &sr.max_range); + + if (sr.max_range < 0.0) + sr.max_range = 0.0; + + if (sr.max_range > 1000.0) + sr.max_range = 1000.0; + } + } + + if (strcmp(argv[x], "-m") == 0) { + z = x + 1; + + if (z <= y && argv[z][0] && argv[z][0] != '-') { + sscanf(argv[z], "%lf", &sr.er_mult); + + if (sr.er_mult < 0.1) + sr.er_mult = 1.0; + + if (sr.er_mult > 1.0e6) + sr.er_mult = 1.0e6; + + sr.earthradius *= sr.er_mult; + } + } + + if (strcmp(argv[x], "-v") == 0) { + z = x + 1; + + if (z < (size_t)argc && argv[z][0] && argv[z][0] != '-') { + int verbose; + sscanf(argv[z], "%d", &verbose); + sr.verbose = verbose != 0; + } + } + + if (strcmp(argv[x], "-gc") == 0) { + z = x + 1; + + if (z <= y && argv[z][0] && argv[z][0] != '-') { + sscanf(argv[z], "%lf", &sr.clutter); + + if (sr.clutter < 0.0) + sr.clutter = 0.0; + } + } + + if (strcmp(argv[x], "-fz") == 0) { + z = x + 1; + + if (z <= y && argv[z][0] && argv[z][0] != '-') { + sscanf(argv[z], "%lf", &sr.fzone_clearance); + + if (sr.fzone_clearance < 0.0 || sr.fzone_clearance > 100.0) + sr.fzone_clearance = 60.0; + + sr.fzone_clearance /= 100.0; + } + } + + if (strcmp(argv[x], "-o") == 0) { + z = x + 1; + + if (z <= y && argv[z][0] && argv[z][0] != '-') + options.mapfile = argv[z]; + sr.map = true; + } + + if (strcmp(argv[x], "-log") == 0) { + z = x + 1; + + if (z <= y && argv[z][0] && argv[z][0] != '-') + options.logfile = argv[z]; + + sr.command_line_log = true; + } + + if (strcmp(argv[x], "-udt") == 0) { + z = x + 1; + + if (z <= y && argv[z][0] && argv[z][0] != '-') + options.udt_file = argv[z]; + } + + if (strcmp(argv[x], "-c") == 0) { + z = x + 1; + + if (z <= y && argv[z][0] && argv[z][0] != '-') { + sscanf(argv[z], "%lf", &sr.altitude); + sr.map = true; + sr.coverage = true; + sr.area_mode = true; + } + } + + if (strcmp(argv[x], "-db") == 0 || strcmp(argv[x], "-dB") == 0) { + z = x + 1; + + if (z <= y && argv[z][0]) /* A minus argument is legal here */ + sscanf(argv[z], "%d", &sr.contour_threshold); + } + + if (strcmp(argv[x], "-p") == 0) { + z = x + 1; + + if (z <= y && argv[z][0] && argv[z][0] != '-') { + options.terrain_file = argv[z]; + sr.terrain_plot = true; + sr.pt2pt_mode = true; + } + } + + if (strcmp(argv[x], "-e") == 0) { + z = x + 1; + + if (z <= y && argv[z][0] && argv[z][0] != '-') { + options.elevation_file = argv[z]; + sr.elevation_plot = true; + sr.pt2pt_mode = true; + } + } + + if (strcmp(argv[x], "-h") == 0 || strcmp(argv[x], "-H") == 0) { + z = x + 1; + + if (z <= y && argv[z][0] && argv[z][0] != '-') { + options.height_file = argv[z]; + sr.height_plot = true; + sr.pt2pt_mode = true; + } + + sr.norm = strcmp(argv[x], "-H") == 0 ? true : false; + } + + bool imagetype_set = false; +#ifdef HAVE_LIBPNG + if (strcmp(argv[x], "-ppm") == 0) { + if (imagetype_set && sr.imagetype != IMAGETYPE_PPM) { + fprintf(stdout, + "-jpg and -ppm are exclusive options, ignoring -ppm.\n"); + } else { + sr.imagetype = IMAGETYPE_PPM; + imagetype_set = true; + } + } +#endif +#ifdef HAVE_LIBGDAL + if (strcmp(argv[x], "-tif") == 0) { + if (imagetype_set && sr.imagetype != IMAGETYPE_PPM) { + fprintf(stdout, + "-tif and -ppm are exclusive options, ignoring -ppm.\n"); + } else { + sr.imagetype = IMAGETYPE_GEOTIFF; + imagetype_set = true; + } + } +#endif +#ifdef HAVE_LIBJPEG + if (strcmp(argv[x], "-jpg") == 0) { + if (imagetype_set && sr.imagetype != IMAGETYPE_JPG) { +#ifdef HAVE_LIBPNG + fprintf(stdout, + "-jpg and -ppm are exclusive options, ignoring -jpg.\n"); +#else + fprintf(stdout, + "-jpg and -png are exclusive options, ignoring -jpg.\n"); +#endif + } else { + sr.imagetype = IMAGETYPE_JPG; + imagetype_set = true; + } + } +#endif + +#ifdef HAVE_LIBGDAL + if (strcmp(argv[x], "-proj") == 0) { + if (sr.imagetype == IMAGETYPE_GEOTIFF || + sr.imagetype == IMAGETYPE_PNG || sr.imagetype == IMAGETYPE_JPG) { + z = x + 1; + if (z <= y && argv[z][0] && argv[z][0] != '-') { + if (strcmp(argv[z], "epsg:3857") == 0) { + sr.projection = PROJ_EPSG_3857; + } else if (strcmp(argv[z], "epsg:4326") == 0) { + sr.projection = PROJ_EPSG_4326; + } else { + std::cerr << "Ignoring unknown projection " << argv[z] + << " and taking epsg:4326 instead.\n"; + } + } + } else { + std::cerr << "-proj supports only gdal output formats. Please " + "use -png, -tif or -jpg.\n"; + } + } +#endif + + if (strcmp(argv[x], "-imperial") == 0) + sr.metric = false; + + if (strcmp(argv[x], "-msl") == 0) + sr.msl = true; + + if (strcmp(argv[x], "-gpsav") == 0) + sr.gpsav = true; + + if (strcmp(argv[x], "-geo") == 0) + sr.geo = true; + + if (strcmp(argv[x], "-kml") == 0) + sr.kml = true; + + if (strcmp(argv[x], "-kmz") == 0) + sr.kmz = true; + + if (strcmp(argv[x], "-json") == 0) + sr.json = true; + + if (strcmp(argv[x], "-nf") == 0) + sr.fresnel_plot = false; + + if (strcmp(argv[x], "-ngs") == 0) + sr.ngs = true; + + if (strcmp(argv[x], "-n") == 0) + sr.nolospath = true; + + if (strcmp(argv[x], "-dbm") == 0) + sr.dbm = true; + + if (strcmp(argv[x], "-sc") == 0) + sr.smooth_contours = true; + + if (strcmp(argv[x], "-st") == 0) + sr.multithread = false; + + if (strcmp(argv[x], "-itwom") == 0) + sr.propagation_model = PROP_ITWOM; + + if (strcmp(argv[x], "-N") == 0) { + sr.nolospath = true; + sr.nositereports = true; + } + + if (strcmp(argv[x], "-d") == 0) { + z = x + 1; + + if (z <= y && argv[z][0] && argv[z][0] != '-') + sr.sdf_path = argv[z]; + } + + if (strcmp(argv[x], "-t") == 0) { + /* Read Transmitter Location */ + + z = x + 1; + + while (z <= y && argv[z][0] && argv[z][0] != '-' && + options.tx_site_files.size() < 30) { + options.tx_site_files.push_back(argv[z]); + z++; + } + + z--; + } + + if (strcmp(argv[x], "-L") == 0) { + z = x + 1; + + if (z <= y && argv[z][0] && argv[z][0] != '-') { + sscanf(argv[z], "%lf", &sr.altitudeLR); + sr.map = true; + sr.LRmap = true; + sr.area_mode = true; + + if (sr.coverage) + fprintf(stdout, + "c and L are exclusive options, ignoring L.\n"); + } + } + + if (strcmp(argv[x], "-l") == 0) { + z = x + 1; + + if (z <= y && argv[z][0] && argv[z][0] != '-') { + options.longley_file = argv[z]; + sr.longley_plot = true; + sr.pt2pt_mode = true; + } + } + + if (strcmp(argv[x], "-r") == 0) { + /* Read Receiver Location */ + + z = x + 1; + + if (z <= y && argv[z][0] && argv[z][0] != '-') { + options.rx_site_file = argv[z]; + sr.rxsite = true; + sr.pt2pt_mode = true; + } + } + + if (strcmp(argv[x], "-s") == 0) { + /* Read city file(s) */ + + z = x + 1; + + while (z <= y && argv[z][0] && argv[z][0] != '-') { + options.city_files.push_back(argv[z]); + z++; + } + + z--; + } + + if (strcmp(argv[x], "-b") == 0) { + /* Read Boundary File(s) */ + + z = x + 1; + + while (z <= y && argv[z][0] && argv[z][0] != '-') { + options.boundary_files.push_back(argv[z]); + z++; + } + + z--; + } + + if (strcmp(argv[x], "-f") == 0) { + z = x + 1; + + if (z <= y && argv[z][0] && argv[z][0] != '-') { + sscanf(argv[z], "%lf", &(sr.forced_freq)); + + if (sr.forced_freq < 20.0) + sr.forced_freq = 0.0; + + if (sr.forced_freq > 20.0e3) + sr.forced_freq = 20.0e3; + } + } + + if (strcmp(argv[x], "-erp") == 0) { + z = x + 1; + + if (z <= y && argv[z][0] && argv[z][0] != '-') { + sscanf(argv[z], "%lf", &(sr.forced_erp)); + + if (sr.forced_erp < 0.0) + sr.forced_erp = -1.0; + } + } + + if (strcmp(argv[x], "-ano") == 0) { + z = x + 1; + + if (z <= y && argv[z][0] && argv[z][0] != '-') + options.ano_filename = argv[z]; + } + + if (strcmp(argv[x], "-ani") == 0) { + z = x + 1; + + if (z <= y && argv[z][0] && argv[z][0] != '-') + options.ani_filename = argv[z]; + } + + if (strcmp(argv[x], "-maxpages") == 0) { + z = x + 1; + + if (z <= y && argv[z][0] && argv[z][0] != '-') { + std::string maxpages_str = argv[z]; + if (sscanf(maxpages_str.c_str(), "%d", &sr.maxpages) != 1) { + options.parse_error = true; + options.error_message = + "Could not parse maxpages: " + maxpages_str; + return false; + } + } + } + + if (strcmp(argv[x], "-sdelim") == 0) { + z = x + 1; + + if (z <= y && argv[z][0] && argv[z][0] != '-') { + sr.sdf_delimiter = argv[z]; + } + } + + if (strcmp(argv[x], "-hd") == 0) { + sr.hd_mode = true; + } + } /* end of command line argument scanning */ + + return true; +} + +bool ValidateCommandLine(const SplatRun &sr, const CommandLineOptions &options) { + // Check for transmitter sites + if (options.tx_site_files.empty()) { + std::cerr << "\n*** ERROR: No transmitter site(s) specified!\n\n"; + return false; + } + + // Validate maxpages + switch (sr.maxpages) { + case 1: + if (!sr.hd_mode) { + std::cerr + << "\n*** ERROR: -maxpages must be >= 4 if not in HD mode!\n\n"; + return false; + } + break; + case 4: + case 9: + case 16: + case 25: + case 36: + case 49: + case 64: + break; + default: + std::cerr << "\n*** ERROR: -maxpages must be one of 1, 4, 9, 16, 25, 36, " + "49, 64\n\n"; + return false; + } + + return true; +} diff --git a/src/command_line_parser.h b/src/command_line_parser.h new file mode 100644 index 0000000..5862b91 --- /dev/null +++ b/src/command_line_parser.h @@ -0,0 +1,73 @@ +/** @file command_line_parser.h + * + * Command-line argument parser for SPLAT! + * Extracts and validates command-line arguments into SplatRun configuration + * + * @copyright 1997 - 2018 John A. Magliacane (KD2BD) and contributors. + * See revision control history for contributions. + * This file is covered by the LICENSE.md file in the root of this project. + */ + +#ifndef COMMAND_LINE_PARSER_H +#define COMMAND_LINE_PARSER_H + +#include "splat_run.h" +#include "site.h" +#include +#include + +/** + * Structure to hold parsed command-line options + */ +struct CommandLineOptions { + // File paths + std::vector tx_site_files; + std::string rx_site_file; + std::vector city_files; + std::vector boundary_files; + std::string mapfile; + std::string elevation_file; + std::string height_file; + std::string longley_file; + std::string terrain_file; + std::string udt_file; + std::string ani_filename; + std::string ano_filename; + std::string logfile; + + // SplatRun configuration (will be populated into sr) + // Most settings will be set directly in sr parameter + + // Parse status + bool show_help = false; + bool parse_error = false; + std::string error_message; +}; + +/** + * Parse command-line arguments and populate SplatRun configuration + * + * @param argc Number of command-line arguments + * @param argv Array of command-line argument strings + * @param sr SplatRun object to populate with parsed settings + * @param options CommandLineOptions structure to populate with file paths + * @return true if parsing was successful, false if error or help requested + */ +bool ParseCommandLine(int argc, const char *argv[], SplatRun &sr, + CommandLineOptions &options); + +/** + * Validate parsed command-line options for consistency + * + * @param sr SplatRun configuration + * @param options Parsed command-line options + * @return true if validation passed, false otherwise + */ +bool ValidateCommandLine(const SplatRun &sr, const CommandLineOptions &options); + +/** + * Print help message showing available command-line options + */ +void PrintHelp(const SplatRun &sr); + +#endif // COMMAND_LINE_PARSER_H diff --git a/src/main.cpp b/src/main.cpp index ac214c5..b1f12e0 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -14,6 +14,7 @@ #include "antenna_pattern.h" #include "boundary_file.h" #include "city_file.h" +#include "command_line_parser.h" #include "dem.h" #include "elevation_map.h" #include "gnuplot.h" @@ -49,17 +50,11 @@ int main(int argc, const char *argv[]) { // This must be done before any ImageWriter objects are created ImageWriter::InitializeGDAL(); - size_t x, y, z = 0; + size_t x, y, z; int min_lat, min_lon, max_lat, max_lon, rxlat, rxlon, txlat, txlon, west_min, west_max, north_min, north_max; char *env = NULL; - std::string mapfile, elevation_file, height_file, longley_file, - terrain_file, udt_file, ani_filename, ano_filename, logfile, - maxpages_str, proj; - - std::vector city_file; - std::vector boundary_file; std::vector tx_site; Site rx_site; @@ -122,506 +117,32 @@ int main(int argc, const char *argv[]) { sr.verbose = 1; sr.sdf_delimiter = "_"; - if (argc == 1 || (argc == 2 && strcmp(argv[1], "--help") == 0)) { - std::cout - << "\n\t\t --==[ " << SplatRun::splat_name << " v" - << SplatRun::splat_version - << " Available Options... ]==--\n\n" - " -t txsite(s).qth\n" - " -r sr.rxsite.qth\n" - " -c plot LOS coverage of TX(s) with an RX antenna at X " - "feet/meters AGL\n" - " -L plot path loss map of TX based on an RX at X " - "feet/meters AGL\n" - " -s filename(s) of city/site file(s) to import (5 max)\n" - " -b filename(s) of cartographic boundary file(s) to " - "import (5 max)\n" - " -p filename of terrain profile graph to plot\n" - " -e filename of terrain elevation graph to plot\n" - " -h filename of terrain height graph to plot\n" - " -H filename of normalized terrain height graph to plot\n" - " -l filename of path loss graph to plot\n" - " -o filename of topographic map to generate (without " - "suffix)\n" - " -d sdf file directory path (overrides path in " - "~/.splat_path file)\n" - " -m earth radius multiplier\n" - " -n do not plot LOS paths in maps\n" - " -N do not produce unnecessary site or obstruction " - "reports\n" - " -f frequency for Fresnel zone calculation (MHz)\n" - " -R modify default range for -c or -L " - "(miles/kilometers)\n" - " -v N verbosity level. Default is 1. Set to 0 to quiet " - "everything.\n" - " -st use a single CPU thread (classic mode)\n" - " -hd Use High Definition mode (3600 ppd vs 1200 ppd). " - "Requires SRTM-1 SDF files.\n" - " -sc display smooth rather than quantized contour levels\n" - " -db threshold beyond which contours will not be " - "displayed\n" - " -nf do not plot Fresnel zones in height plots\n" - " -fz Fresnel zone clearance percentage (default = 60)\n" - " -gc ground clutter height (feet/meters)\n" - " -jpg when generating maps, create jpgs instead of pngs or " - "ppms\n" -#ifdef HAVE_LIBPNG - " -ppm when generating maps, create ppms instead of pngs or " - "jpgs\n" -#endif - " -tif create geotiff instead of png or jpeg\n" - " -ngs display greyscale topography as white in images\n" - " -erp override ERP in .lrp file (Watts)\n" - " -ano name of alphanumeric output file\n" - " -ani name of alphanumeric input file\n" - " -udt name of user defined terrain input file\n" - " -kml generate Google Earth (.kml) compatible output\n" - " -kmz generate Google Earth compressed (.kmz) output\n" - " -geo generate an Xastir .geo georeference file (with " - "image output)\n" - " -dbm plot signal power level contours rather than field " - "strength\n" - " -log copy command line std::string to this output file\n" - " -json create JSON file containing configuration \n" - " -gpsav preserve gnuplot temporary working files after " - "SPLAT! execution\n" - " -itwom invoke the ITWOM model instead of using " - "Longley-Rice\n" - " -imperial employ imperial rather than metric units for all " - "user I/O\n" - " -msl use MSL for TX/RX altitudes instead of AGL\n" - "-maxpages [" - << sr.maxpages - << "] Maximum Analysis Region capability: 1, 4, 9, 16, 25, 36, 49, " - "64 \n" - " -sdelim [" - << sr.sdf_delimiter - << "] Lat and lon delimeter in SDF filenames \n" - "\n" - "See the documentation for more details.\n\n"; - - return 1; - } - - /* Scan for command line arguments */ - y = argc - 1; - - for (x = 1; x <= y; x++) { - if (strcmp(argv[x], "-R") == 0) { - z = x + 1; - - if (z <= y && argv[z][0] && argv[z][0] != '-') { - sscanf(argv[z], "%lf", &sr.max_range); - - if (sr.max_range < 0.0) - sr.max_range = 0.0; - - if (sr.max_range > 1000.0) - sr.max_range = 1000.0; - } - } - - if (strcmp(argv[x], "-m") == 0) { - z = x + 1; - - if (z <= y && argv[z][0] && argv[z][0] != '-') { - sscanf(argv[z], "%lf", &sr.er_mult); - - if (sr.er_mult < 0.1) - sr.er_mult = 1.0; - - if (sr.er_mult > 1.0e6) - sr.er_mult = 1.0e6; - - sr.earthradius *= sr.er_mult; - } - } - - if (strcmp(argv[x], "-v") == 0) { - z = x + 1; - - if (z < (size_t) argc && argv[z][0] && argv[z][0] != '-') { - int verbose; - sscanf(argv[z], "%d", &verbose); - sr.verbose = verbose != 0; - } - } - - if (strcmp(argv[x], "-gc") == 0) { - z = x + 1; - - if (z <= y && argv[z][0] && argv[z][0] != '-') { - sscanf(argv[z], "%lf", &sr.clutter); - - if (sr.clutter < 0.0) - sr.clutter = 0.0; - } - } - - if (strcmp(argv[x], "-fz") == 0) { - z = x + 1; - - if (z <= y && argv[z][0] && argv[z][0] != '-') { - sscanf(argv[z], "%lf", &sr.fzone_clearance); - - if (sr.fzone_clearance < 0.0 || sr.fzone_clearance > 100.0) - sr.fzone_clearance = 60.0; - - sr.fzone_clearance /= 100.0; - } - } - - if (strcmp(argv[x], "-o") == 0) { - z = x + 1; - - if (z <= y && argv[z][0] && argv[z][0] != '-') - mapfile = argv[z]; - sr.map = true; - } - - if (strcmp(argv[x], "-log") == 0) { - z = x + 1; - - if (z <= y && argv[z][0] && argv[z][0] != '-') - logfile = argv[z]; - - sr.command_line_log = true; - } - - if (strcmp(argv[x], "-udt") == 0) { - z = x + 1; - - if (z <= y && argv[z][0] && argv[z][0] != '-') - udt_file = argv[z]; - } - - if (strcmp(argv[x], "-c") == 0) { - z = x + 1; - - if (z <= y && argv[z][0] && argv[z][0] != '-') { - sscanf(argv[z], "%lf", &sr.altitude); - sr.map = true; - sr.coverage = true; - sr.area_mode = true; - } - } - - if (strcmp(argv[x], "-db") == 0 || strcmp(argv[x], "-dB") == 0) { - z = x + 1; - - if (z <= y && argv[z][0]) /* A minus argument is legal here */ - sscanf(argv[z], "%d", &sr.contour_threshold); - } - - if (strcmp(argv[x], "-p") == 0) { - z = x + 1; - - if (z <= y && argv[z][0] && argv[z][0] != '-') { - terrain_file = argv[z]; - sr.terrain_plot = true; - sr.pt2pt_mode = true; - } - } - - if (strcmp(argv[x], "-e") == 0) { - z = x + 1; - - if (z <= y && argv[z][0] && argv[z][0] != '-') { - elevation_file = argv[z]; - sr.elevation_plot = true; - sr.pt2pt_mode = true; - } - } - - if (strcmp(argv[x], "-h") == 0 || strcmp(argv[x], "-H") == 0) { - z = x + 1; - - if (z <= y && argv[z][0] && argv[z][0] != '-') { - height_file = argv[z]; - sr.height_plot = true; - sr.pt2pt_mode = true; - } - - sr.norm = strcmp(argv[x], "-H") == 0 ? true : false; - } - - bool imagetype_set = false; -#ifdef HAVE_LIBPNG - if (strcmp(argv[x], "-ppm") == 0) { - if (imagetype_set && sr.imagetype != IMAGETYPE_PPM) { - fprintf( - stdout, - "-jpg and -ppm are exclusive options, ignoring -ppm.\n"); - } else { - sr.imagetype = IMAGETYPE_PPM; - imagetype_set = true; - } - } -#endif -#ifdef HAVE_LIBGDAL - if (strcmp(argv[x], "-tif") == 0) { - if (imagetype_set && sr.imagetype != IMAGETYPE_PPM) { - fprintf( - stdout, - "-tif and -ppm are exclusive options, ignoring -ppm.\n"); - } else { - sr.imagetype = IMAGETYPE_GEOTIFF; - imagetype_set = true; - } - } -#endif -#ifdef HAVE_LIBJPEG - if (strcmp(argv[x], "-jpg") == 0) { - if (imagetype_set && sr.imagetype != IMAGETYPE_JPG) { -#ifdef HAVE_LIBPNG - fprintf( - stdout, - "-jpg and -ppm are exclusive options, ignoring -jpg.\n"); -#else - fprintf( - stdout, - "-jpg and -png are exclusive options, ignoring -jpg.\n"); -#endif - } else { - sr.imagetype = IMAGETYPE_JPG; - imagetype_set = true; - } - } -#endif - -#ifdef HAVE_LIBGDAL - if (strcmp(argv[x], "-proj") == 0) { - if (sr.imagetype == IMAGETYPE_GEOTIFF || - sr.imagetype == IMAGETYPE_PNG || - sr.imagetype == IMAGETYPE_JPG) { - z = x + 1; - if (z <= y && argv[z][0] && argv[z][0] != '-') { - if (strcmp(argv[z], "epsg:3857") == 0) { - sr.projection = PROJ_EPSG_3857; - } else if (strcmp(argv[z], "epsg:4326") == 0) { - sr.projection = PROJ_EPSG_4326; - } else { - std::cerr << "Ignoring unknown projection " << argv[z] - << " and taking epsg:4326 instead.\n"; - } - } - } else { - std::cerr << "-proj supports only gdal output formats. Please " - "use -png, -tif or -jpg.\n"; - } - } -#endif - - if (strcmp(argv[x], "-imperial") == 0) - sr.metric = false; - - if (strcmp(argv[x], "-msl") == 0) - sr.msl = true; - - if (strcmp(argv[x], "-gpsav") == 0) - sr.gpsav = true; - - if (strcmp(argv[x], "-geo") == 0) - sr.geo = true; - - if (strcmp(argv[x], "-kml") == 0) - sr.kml = true; - - if (strcmp(argv[x], "-kmz") == 0) - sr.kmz = true; - - if (strcmp(argv[x], "-json") == 0) - sr.json = true; - - if (strcmp(argv[x], "-nf") == 0) - sr.fresnel_plot = false; - - if (strcmp(argv[x], "-ngs") == 0) - sr.ngs = true; - - if (strcmp(argv[x], "-n") == 0) - sr.nolospath = true; - - if (strcmp(argv[x], "-dbm") == 0) - sr.dbm = true; - - if (strcmp(argv[x], "-sc") == 0) - sr.smooth_contours = true; - - if (strcmp(argv[x], "-st") == 0) - sr.multithread = false; - - if (strcmp(argv[x], "-itwom") == 0) - sr.propagation_model = PROP_ITWOM; - - if (strcmp(argv[x], "-N") == 0) { - sr.nolospath = true; - sr.nositereports = true; - } - - if (strcmp(argv[x], "-d") == 0) { - z = x + 1; - - if (z <= y && argv[z][0] && argv[z][0] != '-') - sr.sdf_path = argv[z]; - } - - if (strcmp(argv[x], "-t") == 0) { - /* Read Transmitter Location */ - - z = x + 1; - - while (z <= y && argv[z][0] && argv[z][0] != '-' && - tx_site.size() < 30) { - std::string txfile = argv[z]; - tx_site.push_back(Site(txfile)); - z++; - } - - z--; - } - - if (strcmp(argv[x], "-L") == 0) { - z = x + 1; - - if (z <= y && argv[z][0] && argv[z][0] != '-') { - sscanf(argv[z], "%lf", &sr.altitudeLR); - sr.map = true; - sr.LRmap = true; - sr.area_mode = true; - - if (sr.coverage) - fprintf(stdout, - "c and L are exclusive options, ignoring L.\n"); - } - } - - if (strcmp(argv[x], "-l") == 0) { - z = x + 1; - - if (z <= y && argv[z][0] && argv[z][0] != '-') { - longley_file = argv[z]; - sr.longley_plot = true; - sr.pt2pt_mode = true; - } - } - - if (strcmp(argv[x], "-r") == 0) { - /* Read Receiver Location */ - - z = x + 1; - - if (z <= y && argv[z][0] && argv[z][0] != '-') { - std::string rxfile = argv[z]; - rx_site.LoadQTH(rxfile); - sr.rxsite = true; - sr.pt2pt_mode = true; - } - } - - if (strcmp(argv[x], "-s") == 0) { - /* Read city file(s) */ - - z = x + 1; - - while (z <= y && argv[z][0] && argv[z][0] != '-') { - city_file.push_back(argv[z]); - z++; - } - - z--; - } - - if (strcmp(argv[x], "-b") == 0) { - /* Read Boundary File(s) */ - - z = x + 1; - - while (z <= y && argv[z][0] && argv[z][0] != '-') { - boundary_file.push_back(argv[z]); - z++; - } - - z--; - } - - if (strcmp(argv[x], "-f") == 0) { - z = x + 1; - - if (z <= y && argv[z][0] && argv[z][0] != '-') { - sscanf(argv[z], "%lf", &(sr.forced_freq)); - - if (sr.forced_freq < 20.0) - sr.forced_freq = 0.0; - - if (sr.forced_freq > 20.0e3) - sr.forced_freq = 20.0e3; - } - } - - if (strcmp(argv[x], "-erp") == 0) { - z = x + 1; - - if (z <= y && argv[z][0] && argv[z][0] != '-') { - sscanf(argv[z], "%lf", &(sr.forced_erp)); - - if (sr.forced_erp < 0.0) - sr.forced_erp = -1.0; - } - } - - if (strcmp(argv[x], "-ano") == 0) { - z = x + 1; - - if (z <= y && argv[z][0] && argv[z][0] != '-') - ano_filename = argv[z]; - } - - if (strcmp(argv[x], "-ani") == 0) { - z = x + 1; - - if (z <= y && argv[z][0] && argv[z][0] != '-') - ani_filename = argv[z]; - } - - if (strcmp(argv[x], "-maxpages") == 0) { - z = x + 1; - - if (z <= y && argv[z][0] && argv[z][0] != '-') { - maxpages_str = argv[z]; - if (sscanf(maxpages_str.c_str(), "%d", &sr.maxpages) != 1) { - std::cerr << "\n" - << 7 << "*** ERROR: Could not parse maxpages: " - << maxpages_str << "\n\n"; - exit(-1); - } - } - } - - if (strcmp(argv[x], "-sdelim") == 0) { - z = x + 1; - - if (z <= y && argv[z][0] && argv[z][0] != '-') { - sr.sdf_delimiter = argv[z]; - } + // Parse command-line arguments + CommandLineOptions options; + if (!ParseCommandLine(argc, argv, sr, options)) { + if (options.parse_error) { + std::cerr << "\n*** ERROR: " << options.error_message << "\n\n"; + return -1; } + return options.show_help ? 1 : -1; + } - if (strcmp(argv[x], "-hd") == 0) { - sr.hd_mode = true; - } - } /* end of command line argument scanning */ + // Load transmitter sites from parsed filenames + for (const auto &txfile : options.tx_site_files) { + tx_site.push_back(Site(txfile)); + } - /* Perform some error checking on the arguments - and switches parsed from the command-line. - If an error is encountered, print a message - and exit gracefully. */ + // Load receiver site if specified + if (!options.rx_site_file.empty()) { + rx_site.LoadQTH(options.rx_site_file); + } - if (tx_site.size() == 0) { - fprintf(stderr, "\n%c*** ERROR: No transmitter site(s) specified!\n\n", - 7); - exit(-1); + // Validate the parsed configuration + if (!ValidateCommandLine(sr, options)) { + return -1; } + /* Perform additional error checking on the loaded sites */ for (x = 0, y = 0; x < tx_site.size(); x++) { if (tx_site[x].lat == 91.0 && tx_site[x].lon == 361.0) { fprintf(stderr, "\n*** ERROR: Transmitter site #%lu not found!", @@ -635,7 +156,7 @@ int main(int argc, const char *argv[]) { exit(-1); } - if (! sr.coverage && ! sr.LRmap && ani_filename.empty() && + if (!sr.coverage && !sr.LRmap && options.ani_filename.empty() && rx_site.lat == 91.0 && rx_site.lon == 361.0) { if (sr.max_range != 0.0 && tx_site.size() != 0) { /* Plot topographic map of radius "sr.max_range" */ @@ -668,13 +189,6 @@ int main(int argc, const char *argv[]) { switch (sr.maxpages) { case 1: - if (! sr.hd_mode) { - fprintf( - stderr, - "\n%c*** ERROR: -maxpages must be >= 4 if not in HD mode!\n\n", - 7); - exit(-1); - } sr.arraysize = 5092; break; case 4: @@ -704,12 +218,6 @@ int main(int argc, const char *argv[]) { case 64: sr.arraysize = sr.hd_mode ? 230430 : 76810; break; - default: - fprintf(stderr, - "\n%c*** ERROR: -maxpages must be one of 1, 4, 9, 16, 25, 36, " - "49, 64\n\n", - 7); - exit(-1); } sr.ippd = sr.hd_mode ? 3600 : 1200; /* pixels per degree (integer) */ @@ -779,7 +287,7 @@ int main(int argc, const char *argv[]) { CityFile cf; Region region; - if (! ani_filename.empty()) { + if (!options.ani_filename.empty()) { /* read alphanumeric output file from previous simulations if given */ // TODO: Here's an instance where reading the LRParms may say to load @@ -796,7 +304,7 @@ int main(int argc, const char *argv[]) { } Anf anf(lrp, sr); - y = anf.LoadANO(ani_filename, sdf, *em_p); + y = anf.LoadANO(options.ani_filename, sdf, *em_p); for (x = 0; x < tx_site.size(); x++) em_p->PlaceMarker(tx_site[x]); @@ -804,23 +312,23 @@ int main(int argc, const char *argv[]) { if (sr.rxsite) em_p->PlaceMarker(rx_site); - if (boundary_file.size() > 0) { - for (x = 0; x < boundary_file.size(); x++) { - bf.LoadBoundaries(boundary_file[x], *em_p); + if (options.boundary_files.size() > 0) { + for (x = 0; x < options.boundary_files.size(); x++) { + bf.LoadBoundaries(options.boundary_files[x], *em_p); } fprintf(stdout, "\n"); fflush(stdout); } - if (city_file.size() > 0) { - for (x = 0; x < city_file.size(); x++) { - cf.LoadCities(city_file[x], *em_p); + if (options.city_files.size() > 0) { + for (x = 0; x < options.city_files.size(); x++) { + cf.LoadCities(options.city_files[x], *em_p); } fprintf(stdout, "\n"); fflush(stdout); } - Image image(sr, mapfile, tx_site, *em_p); + Image image(sr, options.mapfile, tx_site, *em_p); if (lrp.erp == 0.0) { image.WriteCoverageMap(MAPTYPE_PATHLOSS, sr.imagetype, region); } else { @@ -1004,9 +512,9 @@ int main(int argc, const char *argv[]) { em_p->LoadTopoData(max_lon, min_lon, max_lat, min_lat, sdf); } - if (! udt_file.empty()) { + if (!options.udt_file.empty()) { Udt udt(sr); - udt.LoadUDT(udt_file, *em_p); + udt.LoadUDT(options.udt_file, *em_p); } /***** Let the SPLATting begin! *****/ @@ -1021,27 +529,27 @@ int main(int argc, const char *argv[]) { /* Extract extension (if present) from "terrain_file" */ - ext = Utilities::DivideExtension(terrain_file, "png"); + ext = Utilities::DivideExtension(options.terrain_file, "png"); } if (sr.elevation_plot) { /* Extract extension (if present) from "elevation_file" */ - ext = Utilities::DivideExtension(elevation_file, "png"); + ext = Utilities::DivideExtension(options.elevation_file, "png"); } if (sr.height_plot) { /* Extract extension (if present) from "height_file" */ - ext = Utilities::DivideExtension(height_file, "png"); + ext = Utilities::DivideExtension(options.height_file, "png"); } if (sr.longley_plot) { /* Extract extension (if present) from "longley_file" */ - ext = Utilities::DivideExtension(longley_file, "png"); + ext = Utilities::DivideExtension(options.longley_file, "png"); } for (x = 0; x < tx_site.size() && x < 4; x++) { @@ -1088,9 +596,9 @@ int main(int argc, const char *argv[]) { oss << "." << ext; AntennaPattern pat; - if (! sr.nositereports) { - filename = longley_file + oss.str(); - bool longly_file_exists = ! longley_file.empty(); + if (!sr.nositereports) { + filename = options.longley_file + oss.str(); + bool longly_file_exists = !options.longley_file.empty(); bool loadPat; std::string patFilename; @@ -1115,17 +623,17 @@ int main(int argc, const char *argv[]) { GnuPlot gnuPlot(sr); if (sr.terrain_plot) { - filename = terrain_file + oss.str(); + filename = options.terrain_file + oss.str(); gnuPlot.GraphTerrain(tx_site[x], rx_site, filename, *em_p); } if (sr.elevation_plot) { - filename = elevation_file + oss.str(); + filename = options.elevation_file + oss.str(); gnuPlot.GraphElevation(tx_site[x], rx_site, filename, *em_p); } if (sr.height_plot) { - filename = height_file + oss.str(); + filename = options.height_file + oss.str(); gnuPlot.GraphHeight(tx_site[x], rx_site, filename, sr.fresnel_plot, sr.norm, *em_p, lrp); } @@ -1149,7 +657,7 @@ int main(int argc, const char *argv[]) { } if (flag) { - em_p->PlotLRMap(tx_site[x], sr.altitudeLR, ano_filename, + em_p->PlotLRMap(tx_site[x], sr.altitudeLR, options.ano_filename, *p_pat, lrp); } } @@ -1162,16 +670,16 @@ int main(int argc, const char *argv[]) { if (sr.map || sr.topomap) { /* Label the map */ - if (! (sr.kml || sr.imagetype == IMAGETYPE_GEOTIFF)) { + if (!(sr.kml || sr.imagetype == IMAGETYPE_GEOTIFF)) { for (x = 0; x < tx_site.size(); x++) em_p->PlaceMarker(tx_site[x]); } - if (city_file.size() > 0) { + if (options.city_files.size() > 0) { CityFile cityFile; - for (y = 0; y < city_file.size(); y++) - cityFile.LoadCities(city_file[y], *em_p); + for (y = 0; y < options.city_files.size(); y++) + cityFile.LoadCities(options.city_files[y], *em_p); fprintf(stdout, "\n"); fflush(stdout); @@ -1179,18 +687,18 @@ int main(int argc, const char *argv[]) { /* Load city and county boundary data files */ - if (boundary_file.size() > 0) { + if (options.boundary_files.size() > 0) { BoundaryFile boundaryFile(sr); - for (y = 0; y < boundary_file.size(); y++) - boundaryFile.LoadBoundaries(boundary_file[y], *em_p); + for (y = 0; y < options.boundary_files.size(); y++) + boundaryFile.LoadBoundaries(options.boundary_files[y], *em_p); fprintf(stdout, "\n"); fflush(stdout); } /* Plot the map */ - Image image(sr, mapfile, tx_site, *em_p); + Image image(sr, options.mapfile, tx_site, *em_p); if (sr.coverage || sr.pt2pt_mode || sr.topomap) { image.WriteCoverageMap(MAPTYPE_LOS, sr.imagetype, region); // TODO: PVW: Remove commented out line @@ -1205,21 +713,21 @@ int main(int argc, const char *argv[]) { } } - if (sr.command_line_log && ! logfile.empty()) { + if (sr.command_line_log && !options.logfile.empty()) { std::fstream fs; - fs.open(logfile.c_str(), std::fstream::out); + fs.open(options.logfile.c_str(), std::fstream::out); // TODO: Should we fail silently if we can't open the logfile. Shouldn't // we WARN? if (fs) { - for (x = 0; x < (size_t) argc; x++) { + for (x = 0; x < (size_t)argc; x++) { fs << argv[x] << " "; } fs << std::endl; fs.close(); std::cout << "\nCommand-line parameter log written to: \"" - << logfile << "\"\n"; + << options.logfile << "\"\n"; } } diff --git a/tests/command_line_parser_test.cpp b/tests/command_line_parser_test.cpp new file mode 100644 index 0000000..83134ff --- /dev/null +++ b/tests/command_line_parser_test.cpp @@ -0,0 +1,877 @@ +/** @file command_line_parser_test.cpp + * + * Unit tests for command-line argument parser + * + * @copyright 1997 - 2018 John A. Magliacane (KD2BD) and contributors. + */ + +#include "../src/command_line_parser.h" +#include "../src/splat_run.h" +#include +#include + +// Helper function to create argv array from vector of strings +std::vector MakeArgv(const std::vector &args) { + std::vector argv; + for (const auto &arg : args) { + argv.push_back(arg.c_str()); + } + return argv; +} + +// Test fixture for command-line parser +class CommandLineParserTest : public ::testing::Test { + protected: + void SetUp() override { + // Initialize SplatRun with defaults + sr.maxpages = 16; + sr.arraysize = -1; + sr.propagation_model = PROP_ITM; + sr.hd_mode = false; + sr.coverage = false; + sr.LRmap = false; + sr.terrain_plot = false; + sr.elevation_plot = false; + sr.height_plot = false; + sr.map = false; + sr.longley_plot = false; + sr.norm = false; + sr.topomap = false; + sr.geo = false; + sr.kml = false; + sr.json = false; + sr.pt2pt_mode = false; + sr.area_mode = false; + sr.ngs = false; + sr.nolospath = false; + sr.nositereports = false; + sr.fresnel_plot = true; + sr.command_line_log = false; + sr.rxsite = false; + sr.metric = true; + sr.msl = false; + sr.dbm = false; + sr.bottom_legend = true; + sr.smooth_contours = false; + sr.altitude = 0.0; + sr.altitudeLR = 0.0; + sr.tx_range = 0.0; + sr.rx_range = 0.0; + sr.deg_range = 0.0; + sr.deg_limit = 0.0; + sr.max_range = 0.0; + sr.clutter = 0.0; + sr.forced_erp = -1.0; + sr.forced_freq = 0.0; + sr.fzone_clearance = 0.6; + sr.contour_threshold = 0; + sr.rx_site.lat = 91.0; + sr.rx_site.lon = 361.0; + sr.earthradius = EARTHRADIUS; +#ifdef HAVE_LIBPNG + sr.imagetype = IMAGETYPE_PNG; +#else + sr.imagetype = IMAGETYPE_PPM; +#endif + sr.projection = PROJ_EPSG_4326; + sr.multithread = true; + sr.verbose = 1; + sr.sdf_delimiter = "_"; + } + + SplatRun sr; + CommandLineOptions options; +}; + +// Test help flag +TEST_F(CommandLineParserTest, ShowHelp) { + std::vector args = {"splat", "--help"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_FALSE(result); + EXPECT_TRUE(options.show_help); +} + +// Test no arguments +TEST_F(CommandLineParserTest, NoArguments) { + std::vector args = {"splat"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_FALSE(result); + EXPECT_TRUE(options.show_help); +} + +// Test -t (transmitter site) flag +TEST_F(CommandLineParserTest, TransmitterSingleSite) { + std::vector args = {"splat", "-t", "tx1.qth"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + ASSERT_EQ(options.tx_site_files.size(), 1); + EXPECT_EQ(options.tx_site_files[0], "tx1.qth"); +} + +TEST_F(CommandLineParserTest, TransmitterMultipleSites) { + std::vector args = {"splat", "-t", "tx1.qth", "tx2.qth", + "tx3.qth"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + ASSERT_EQ(options.tx_site_files.size(), 3); + EXPECT_EQ(options.tx_site_files[0], "tx1.qth"); + EXPECT_EQ(options.tx_site_files[1], "tx2.qth"); + EXPECT_EQ(options.tx_site_files[2], "tx3.qth"); +} + +// Test -r (receiver site) flag +TEST_F(CommandLineParserTest, ReceiverSite) { + std::vector args = {"splat", "-t", "tx.qth", "-r", "rx.qth"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + EXPECT_EQ(options.rx_site_file, "rx.qth"); + EXPECT_TRUE(sr.rxsite); + EXPECT_TRUE(sr.pt2pt_mode); +} + +// Test -c (coverage) flag +TEST_F(CommandLineParserTest, CoverageMode) { + std::vector args = {"splat", "-t", "tx.qth", "-c", "10.0"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + EXPECT_TRUE(sr.coverage); + EXPECT_TRUE(sr.map); + EXPECT_TRUE(sr.area_mode); + EXPECT_DOUBLE_EQ(sr.altitude, 10.0); +} + +// Test -L (path loss map) flag +TEST_F(CommandLineParserTest, PathLossMap) { + std::vector args = {"splat", "-t", "tx.qth", "-L", "25.5"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + EXPECT_TRUE(sr.LRmap); + EXPECT_TRUE(sr.map); + EXPECT_TRUE(sr.area_mode); + EXPECT_DOUBLE_EQ(sr.altitudeLR, 25.5); +} + +// Test -s (city files) flag +TEST_F(CommandLineParserTest, CityFiles) { + std::vector args = {"splat", "-t", "tx.qth", "-s", + "city1.dat", "city2.dat"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + ASSERT_EQ(options.city_files.size(), 2); + EXPECT_EQ(options.city_files[0], "city1.dat"); + EXPECT_EQ(options.city_files[1], "city2.dat"); +} + +// Test -b (boundary files) flag +TEST_F(CommandLineParserTest, BoundaryFiles) { + std::vector args = {"splat", "-t", "tx.qth", "-b", + "bound1.dat", "bound2.dat", "bound3.dat"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + ASSERT_EQ(options.boundary_files.size(), 3); + EXPECT_EQ(options.boundary_files[0], "bound1.dat"); + EXPECT_EQ(options.boundary_files[1], "bound2.dat"); + EXPECT_EQ(options.boundary_files[2], "bound3.dat"); +} + +// Test -p (terrain profile) flag +TEST_F(CommandLineParserTest, TerrainProfile) { + std::vector args = {"splat", "-t", "tx.qth", "-r", "rx.qth", + "-p", "terrain.png"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + EXPECT_EQ(options.terrain_file, "terrain.png"); + EXPECT_TRUE(sr.terrain_plot); + EXPECT_TRUE(sr.pt2pt_mode); +} + +// Test -e (elevation plot) flag +TEST_F(CommandLineParserTest, ElevationPlot) { + std::vector args = {"splat", "-t", "tx.qth", "-r", "rx.qth", + "-e", "elevation.png"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + EXPECT_EQ(options.elevation_file, "elevation.png"); + EXPECT_TRUE(sr.elevation_plot); + EXPECT_TRUE(sr.pt2pt_mode); +} + +// Test -h (height plot) flag +TEST_F(CommandLineParserTest, HeightPlot) { + std::vector args = {"splat", "-t", "tx.qth", "-r", "rx.qth", + "-h", "height.png"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + EXPECT_EQ(options.height_file, "height.png"); + EXPECT_TRUE(sr.height_plot); + EXPECT_TRUE(sr.pt2pt_mode); + EXPECT_FALSE(sr.norm); +} + +// Test -H (normalized height plot) flag +TEST_F(CommandLineParserTest, NormalizedHeightPlot) { + std::vector args = {"splat", "-t", "tx.qth", "-r", "rx.qth", + "-H", "height_norm.png"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + EXPECT_EQ(options.height_file, "height_norm.png"); + EXPECT_TRUE(sr.height_plot); + EXPECT_TRUE(sr.pt2pt_mode); + EXPECT_TRUE(sr.norm); +} + +// Test -l (longley plot) flag +TEST_F(CommandLineParserTest, LongleyPlot) { + std::vector args = {"splat", "-t", "tx.qth", "-r", "rx.qth", + "-l", "longley.png"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + EXPECT_EQ(options.longley_file, "longley.png"); + EXPECT_TRUE(sr.longley_plot); + EXPECT_TRUE(sr.pt2pt_mode); +} + +// Test -o (output map) flag +TEST_F(CommandLineParserTest, OutputMap) { + std::vector args = {"splat", "-t", "tx.qth", "-o", "output"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + EXPECT_EQ(options.mapfile, "output"); + EXPECT_TRUE(sr.map); +} + +// Test -d (SDF directory) flag +TEST_F(CommandLineParserTest, SDFDirectory) { + std::vector args = {"splat", "-t", "tx.qth", "-d", + "/path/to/sdf"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + EXPECT_EQ(sr.sdf_path, "/path/to/sdf"); +} + +// Test -m (earth radius multiplier) flag +TEST_F(CommandLineParserTest, EarthRadiusMultiplier) { + std::vector args = {"splat", "-t", "tx.qth", "-m", "1.333"}; + auto argv = MakeArgv(args); + + double original_radius = sr.earthradius; + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + EXPECT_DOUBLE_EQ(sr.er_mult, 1.333); + EXPECT_DOUBLE_EQ(sr.earthradius, original_radius * 1.333); +} + +TEST_F(CommandLineParserTest, EarthRadiusMultiplierTooSmall) { + std::vector args = {"splat", "-t", "tx.qth", "-m", "0.05"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + EXPECT_DOUBLE_EQ(sr.er_mult, 1.0); // Should be clamped to 1.0 +} + +TEST_F(CommandLineParserTest, EarthRadiusMultiplierTooLarge) { + std::vector args = {"splat", "-t", "tx.qth", "-m", "2000000.0"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + EXPECT_DOUBLE_EQ(sr.er_mult, 1.0e6); // Should be clamped to 1e6 +} + +// Test -n (no LOS path) flag +TEST_F(CommandLineParserTest, NoLOSPath) { + std::vector args = {"splat", "-t", "tx.qth", "-n"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + EXPECT_TRUE(sr.nolospath); +} + +// Test -N (no site reports) flag +TEST_F(CommandLineParserTest, NoSiteReports) { + std::vector args = {"splat", "-t", "tx.qth", "-N"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + EXPECT_TRUE(sr.nolospath); + EXPECT_TRUE(sr.nositereports); +} + +// Test -f (frequency) flag +TEST_F(CommandLineParserTest, Frequency) { + std::vector args = {"splat", "-t", "tx.qth", "-f", "915.0"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + EXPECT_DOUBLE_EQ(sr.forced_freq, 915.0); +} + +TEST_F(CommandLineParserTest, FrequencyTooLow) { + std::vector args = {"splat", "-t", "tx.qth", "-f", "10.0"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + EXPECT_DOUBLE_EQ(sr.forced_freq, 0.0); // Should be set to 0 +} + +TEST_F(CommandLineParserTest, FrequencyTooHigh) { + std::vector args = {"splat", "-t", "tx.qth", "-f", "25000.0"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + EXPECT_DOUBLE_EQ(sr.forced_freq, 20.0e3); // Should be clamped to 20000 +} + +// Test -R (range) flag +TEST_F(CommandLineParserTest, Range) { + std::vector args = {"splat", "-t", "tx.qth", "-R", "50.0"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + EXPECT_DOUBLE_EQ(sr.max_range, 50.0); +} + +TEST_F(CommandLineParserTest, RangeNegative) { + std::vector args = {"splat", "-t", "tx.qth", "-R", "-10.0"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + EXPECT_DOUBLE_EQ(sr.max_range, 0.0); // Should be clamped to 0 +} + +TEST_F(CommandLineParserTest, RangeTooLarge) { + std::vector args = {"splat", "-t", "tx.qth", "-R", "1500.0"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + EXPECT_DOUBLE_EQ(sr.max_range, 1000.0); // Should be clamped to 1000 +} + +// Test -v (verbosity) flag +TEST_F(CommandLineParserTest, Verbosity) { + std::vector args = {"splat", "-t", "tx.qth", "-v", "0"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + EXPECT_EQ(sr.verbose, 0); +} + +// Test -st (single thread) flag +TEST_F(CommandLineParserTest, SingleThread) { + std::vector args = {"splat", "-t", "tx.qth", "-st"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + EXPECT_FALSE(sr.multithread); +} + +// Test -hd (high definition) flag +TEST_F(CommandLineParserTest, HighDefinition) { + std::vector args = {"splat", "-t", "tx.qth", "-hd"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + EXPECT_TRUE(sr.hd_mode); +} + +// Test -sc (smooth contours) flag +TEST_F(CommandLineParserTest, SmoothContours) { + std::vector args = {"splat", "-t", "tx.qth", "-sc"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + EXPECT_TRUE(sr.smooth_contours); +} + +// Test -db (contour threshold) flag +TEST_F(CommandLineParserTest, ContourThreshold) { + std::vector args = {"splat", "-t", "tx.qth", "-db", "-100"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + EXPECT_EQ(sr.contour_threshold, -100); +} + +TEST_F(CommandLineParserTest, ContourThresholdAlternateCase) { + std::vector args = {"splat", "-t", "tx.qth", "-dB", "-90"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + EXPECT_EQ(sr.contour_threshold, -90); +} + +// Test -nf (no fresnel) flag +TEST_F(CommandLineParserTest, NoFresnel) { + std::vector args = {"splat", "-t", "tx.qth", "-nf"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + EXPECT_FALSE(sr.fresnel_plot); +} + +// Test -fz (fresnel zone clearance) flag +TEST_F(CommandLineParserTest, FresnelZoneClearance) { + std::vector args = {"splat", "-t", "tx.qth", "-fz", "80"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + EXPECT_DOUBLE_EQ(sr.fzone_clearance, 0.8); // 80% -> 0.8 +} + +TEST_F(CommandLineParserTest, FresnelZoneClearanceNegative) { + std::vector args = {"splat", "-t", "tx.qth", "-fz", "-10"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + EXPECT_DOUBLE_EQ(sr.fzone_clearance, 0.6); // Should default to 60% +} + +TEST_F(CommandLineParserTest, FresnelZoneClearanceTooLarge) { + std::vector args = {"splat", "-t", "tx.qth", "-fz", "150"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + EXPECT_DOUBLE_EQ(sr.fzone_clearance, 0.6); // Should default to 60% +} + +// Test -gc (ground clutter) flag +TEST_F(CommandLineParserTest, GroundClutter) { + std::vector args = {"splat", "-t", "tx.qth", "-gc", "30.0"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + EXPECT_DOUBLE_EQ(sr.clutter, 30.0); +} + +TEST_F(CommandLineParserTest, GroundClutterNegative) { + std::vector args = {"splat", "-t", "tx.qth", "-gc", "-5.0"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + EXPECT_DOUBLE_EQ(sr.clutter, 0.0); // Should be clamped to 0 +} + +// Test image format flags +#ifdef HAVE_LIBPNG +TEST_F(CommandLineParserTest, PPMFormat) { + std::vector args = {"splat", "-t", "tx.qth", "-ppm"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + EXPECT_EQ(sr.imagetype, IMAGETYPE_PPM); +} +#endif + +#ifdef HAVE_LIBJPEG +TEST_F(CommandLineParserTest, JPGFormat) { + std::vector args = {"splat", "-t", "tx.qth", "-jpg"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + EXPECT_EQ(sr.imagetype, IMAGETYPE_JPG); +} +#endif + +#ifdef HAVE_LIBGDAL +TEST_F(CommandLineParserTest, TIFFormat) { + std::vector args = {"splat", "-t", "tx.qth", "-tif"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + EXPECT_EQ(sr.imagetype, IMAGETYPE_GEOTIFF); +} +#endif + +// Test -ngs (no greyscale) flag +TEST_F(CommandLineParserTest, NoGreyscale) { + std::vector args = {"splat", "-t", "tx.qth", "-ngs"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + EXPECT_TRUE(sr.ngs); +} + +// Test -erp (effective radiated power) flag +TEST_F(CommandLineParserTest, ERP) { + std::vector args = {"splat", "-t", "tx.qth", "-erp", "1000.0"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + EXPECT_DOUBLE_EQ(sr.forced_erp, 1000.0); +} + +TEST_F(CommandLineParserTest, ERPNegative) { + std::vector args = {"splat", "-t", "tx.qth", "-erp", "-10.0"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + EXPECT_DOUBLE_EQ(sr.forced_erp, -1.0); // Should be set to -1 +} + +// Test -ano (alphanumeric output) flag +TEST_F(CommandLineParserTest, AlphanumericOutput) { + std::vector args = {"splat", "-t", "tx.qth", "-ano", + "output.txt"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + EXPECT_EQ(options.ano_filename, "output.txt"); +} + +// Test -ani (alphanumeric input) flag +TEST_F(CommandLineParserTest, AlphanumericInput) { + std::vector args = {"splat", "-t", "tx.qth", "-ani", + "input.txt"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + EXPECT_EQ(options.ani_filename, "input.txt"); +} + +// Test -udt (user defined terrain) flag +TEST_F(CommandLineParserTest, UserDefinedTerrain) { + std::vector args = {"splat", "-t", "tx.qth", "-udt", + "terrain.udt"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + EXPECT_EQ(options.udt_file, "terrain.udt"); +} + +// Test -kml flag +TEST_F(CommandLineParserTest, KMLOutput) { + std::vector args = {"splat", "-t", "tx.qth", "-kml"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + EXPECT_TRUE(sr.kml); +} + +// Test -kmz flag +TEST_F(CommandLineParserTest, KMZOutput) { + std::vector args = {"splat", "-t", "tx.qth", "-kmz"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + EXPECT_TRUE(sr.kmz); +} + +// Test -geo flag +TEST_F(CommandLineParserTest, GeoOutput) { + std::vector args = {"splat", "-t", "tx.qth", "-geo"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + EXPECT_TRUE(sr.geo); +} + +// Test -dbm flag +TEST_F(CommandLineParserTest, DBMOutput) { + std::vector args = {"splat", "-t", "tx.qth", "-dbm"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + EXPECT_TRUE(sr.dbm); +} + +// Test -log flag +TEST_F(CommandLineParserTest, LogOutput) { + std::vector args = {"splat", "-t", "tx.qth", "-log", + "command.log"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + EXPECT_TRUE(sr.command_line_log); + EXPECT_EQ(options.logfile, "command.log"); +} + +// Test -json flag +TEST_F(CommandLineParserTest, JSONOutput) { + std::vector args = {"splat", "-t", "tx.qth", "-json"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + EXPECT_TRUE(sr.json); +} + +// Test -gpsav flag +TEST_F(CommandLineParserTest, GnuplotSave) { + std::vector args = {"splat", "-t", "tx.qth", "-gpsav"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + EXPECT_TRUE(sr.gpsav); +} + +// Test -itwom flag +TEST_F(CommandLineParserTest, ITWOMModel) { + std::vector args = {"splat", "-t", "tx.qth", "-itwom"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + EXPECT_EQ(sr.propagation_model, PROP_ITWOM); +} + +// Test -imperial flag +TEST_F(CommandLineParserTest, ImperialUnits) { + std::vector args = {"splat", "-t", "tx.qth", "-imperial"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + EXPECT_FALSE(sr.metric); +} + +// Test -msl flag +TEST_F(CommandLineParserTest, MSLAltitude) { + std::vector args = {"splat", "-t", "tx.qth", "-msl"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + EXPECT_TRUE(sr.msl); +} + +// Test -maxpages flag +TEST_F(CommandLineParserTest, MaxPages) { + std::vector args = {"splat", "-t", "tx.qth", "-maxpages", "25"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + EXPECT_EQ(sr.maxpages, 25); +} + +TEST_F(CommandLineParserTest, MaxPagesInvalid) { + std::vector args = {"splat", "-t", "tx.qth", "-maxpages", + "invalid"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_FALSE(result); + EXPECT_TRUE(options.parse_error); +} + +// Test -sdelim flag +TEST_F(CommandLineParserTest, SDFDelimiter) { + std::vector args = {"splat", "-t", "tx.qth", "-sdelim", ":"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + EXPECT_EQ(sr.sdf_delimiter, ":"); +} + +// Test validation - no transmitter sites +TEST_F(CommandLineParserTest, ValidateNoTransmitter) { + bool result = ValidateCommandLine(sr, options); + + EXPECT_FALSE(result); +} + +// Test validation - valid configuration +TEST_F(CommandLineParserTest, ValidateValidConfiguration) { + options.tx_site_files.push_back("tx.qth"); + sr.maxpages = 16; + + bool result = ValidateCommandLine(sr, options); + + EXPECT_TRUE(result); +} + +// Test validation - invalid maxpages +TEST_F(CommandLineParserTest, ValidateInvalidMaxPages) { + options.tx_site_files.push_back("tx.qth"); + sr.maxpages = 7; // Invalid value + + bool result = ValidateCommandLine(sr, options); + + EXPECT_FALSE(result); +} + +// Test validation - maxpages=1 without HD mode +TEST_F(CommandLineParserTest, ValidateMaxPages1WithoutHD) { + options.tx_site_files.push_back("tx.qth"); + sr.maxpages = 1; + sr.hd_mode = false; + + bool result = ValidateCommandLine(sr, options); + + EXPECT_FALSE(result); +} + +// Test validation - maxpages=1 with HD mode +TEST_F(CommandLineParserTest, ValidateMaxPages1WithHD) { + options.tx_site_files.push_back("tx.qth"); + sr.maxpages = 1; + sr.hd_mode = true; + + bool result = ValidateCommandLine(sr, options); + + EXPECT_TRUE(result); +} + +// Test multiple flags combined +TEST_F(CommandLineParserTest, MultipleFlagsCombined) { + std::vector args = {"splat", "-t", "tx.qth", "-r", + "rx.qth", "-hd", "-itwom", "-imperial", + "-nf", "-sc", "-maxpages", "36"}; + auto argv = MakeArgv(args); + + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + EXPECT_EQ(options.tx_site_files.size(), 1); + EXPECT_EQ(options.rx_site_file, "rx.qth"); + EXPECT_TRUE(sr.hd_mode); + EXPECT_EQ(sr.propagation_model, PROP_ITWOM); + EXPECT_FALSE(sr.metric); + EXPECT_FALSE(sr.fresnel_plot); + EXPECT_TRUE(sr.smooth_contours); + EXPECT_EQ(sr.maxpages, 36); +} + +// Test conflicting options (coverage and path loss) +TEST_F(CommandLineParserTest, ConflictingCoverageAndPathLoss) { + std::vector args = {"splat", "-t", "tx.qth", "-c", + "10.0", "-L", "20.0"}; + auto argv = MakeArgv(args); + + // Parser allows both, but coverage takes precedence (L ignored with warning) + bool result = ParseCommandLine(args.size(), argv.data(), sr, options); + + EXPECT_TRUE(result); + EXPECT_TRUE(sr.coverage); + EXPECT_TRUE(sr.LRmap); +}