diff --git a/CMakeLists.txt b/CMakeLists.txt index ba4faf6..e372576 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,7 +8,7 @@ include(cmake/prelude.cmake) project( lob - VERSION 0.6.5 + VERSION 0.7.0 DESCRIPTION "an exterior balistics calculation library" HOMEPAGE_URL "https://github.com/joelbenway/lob" LANGUAGES CXX) diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt index a9951df..e29368d 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -5,7 +5,7 @@ cmake_minimum_required(VERSION 3.14) project( lobExamples - VERSION 1.0.2 + VERSION 1.0.3 DESCRIPTION "Examples using the lob library" HOMEPAGE_URL "https://github.com/joelbenway/lob" LANGUAGES CXX) diff --git a/example/lobber.cpp b/example/lobber.cpp index 6aa9e5a..23b3fff 100644 --- a/example/lobber.cpp +++ b/example/lobber.cpp @@ -10,11 +10,18 @@ #include #include #include +#include #include #include #include #include +#ifdef _WIN32 +#include +#else +#include +#endif + #include "lob/lob.hpp" #include "version.hpp" @@ -42,7 +49,7 @@ void PrintVersion() { } void PrintHelp() { - std::cout << "Usage: lobber [options]\n" + std::cout << "Usage: lobber [options] [< input.json]\n" << "Options:\n" << " --h, --help Show this help message\n" << " --v, --version Show version information\n" @@ -51,6 +58,9 @@ void PrintHelp() { "instead of user prompts\n" << " --of=FILE Output json file where data is saved for " "future use as an input file\n" + << "\n" + << "Note: When run interactively, a wizard prompts for input.\n" + << " When stdin is redirected, JSON data is read from stdin.\n" << "Example:\n" << colors::kYellow << " lobber --of=my_rifle_load.json\n\n" << colors::kReset; @@ -63,6 +73,15 @@ void PrintGreeting() { "ballistics library. Follow the prompts to enter data.\n\n"; } +// Returns true if the program is being run in an interactive terminal. +bool IsInteractive() { +#ifdef _WIN32 + return _isatty(_fileno(stdin)) != 0; +#else + return isatty(STDIN_FILENO) != 0; +#endif +} + lob::DragFunctionT ConvertDF(double input) { switch (static_cast(std::round(input))) { case 2: // NOLINT(cppcoreguidelines-avoid-magic-numbers, @@ -509,14 +528,18 @@ void PrintExtraInfo(const lob::Input& input) { const auto kSFw = static_cast(kSF.length() + kExtra); const std::string kSS("Speed of Sound"); const auto kSSw = static_cast(kSS.length() + kExtra); + const std::string kE("Error"); + const auto kEw = static_cast(kE.length() + kExtra); std::cout << colors::kYellow << std::left << std::setw(kZAw) << kZA << std::setw(kSFw) << kSF << std::setw(kSSw) << kSS - << colors::kReset << "\n"; + << std::setw(kEw) << kE << colors::kReset << "\n"; std::cout << std::left << std::setw(kZAw) << std::fixed << std::setprecision(2) << input.zero_angle << std::setw(kSFw) << input.stability_factor << std::setw(kSSw) << input.speed_of_sound - << "\n\n"; + << std::setw(kEw) << std::hex << std::showbase + << static_cast(input.error) << std::dec + << std::noshowbase << "\n\n"; } void PrintSolutionTable(const lob::Output* psolutions, size_t size) { @@ -618,22 +641,31 @@ int main(int argc, char* argv[]) { } if (json.empty()) { - try { - std::cin >> json; - } catch (const nlohmann::json::parse_error& e) { - std::cerr << colors::kRed - << "Error parsing JSON from stdin: " << colors::kReset - << e.what() << "\n"; - return 1; + if (example::IsInteractive()) { + for (const auto& pair : example::GetStateKeys()) { + json[pair.second] = "nan"; + } + example::PrintGreeting(); + example::PromptWizard(&json); + } else { + if (std::cin.peek() != std::char_traits::eof()) { + try { + std::cin >> json; + } catch (const nlohmann::json::parse_error& e) { + std::cerr << colors::kRed + << "Error parsing JSON from stdin: " << colors::kReset + << e.what() << "\n"; + return 1; + } + } } } if (json.empty()) { - for (const auto& pair : example::GetStateKeys()) { - json[pair.second] = "nan"; - } - example::PrintGreeting(); - example::PromptWizard(&json); + std::cerr << colors::kRed << "Error: No input data provided." + << colors::kReset << "\n\n"; + example::PrintHelp(); + return 1; } using example::StateType; diff --git a/flake.nix b/flake.nix index d423417..fc0d0b7 100644 --- a/flake.nix +++ b/flake.nix @@ -49,6 +49,32 @@ cmake --install build --prefix $out ''; }; + lobber = pkgs.stdenv.mkDerivation { + name = "lobber"; + src = self; + nativeBuildInputs = with pkgs; [ + cmake + nlohmann_json + ]; + configurePhase = '' + cmake -S . -B build \ + -D CMAKE_BUILD_TYPE=Release \ + -D LOB_DEVELOPER_MODE=OFF \ + -D BUILD_EXAMPLES=ON \ + -D BUILD_BENCHMARKS=OFF + ''; + buildPhase = '' + cmake --build build --parallel $NIX_BUILD_CORES + ''; + installPhase = '' + mkdir -p $out/bin + if [ ! -f build/example/lobber ]; then + echo "Error: lobber binary not found at build/example/lobber" + exit 1 + fi + cp build/example/lobber $out/bin/ + ''; + }; }); devShells = forEachSupportedSystem ({pkgs}: let baseShell = @@ -101,13 +127,15 @@ buildInputs = oldAttrs.buildInputs ++ extraDevPackages; shellHook = let inherit (pkgs) stdenv; - filename = "CMakeUserPresets.json"; + clangdFile = ".clangd"; + CMakeUserPresetsFile = "CMakeUserPresets.json"; os = if stdenv.isLinux then "linux" else if stdenv.isDarwin then "darwin" else ""; + sourceDir = "\\\${sourceDir}"; in '' json=$(cat <<-EOF @@ -121,7 +149,7 @@ "configurePresets": [ { "name": "dev", - "binaryDir": "/build/dev", + "binaryDir": "${sourceDir}/build/dev", "inherits": ["dev-mode", "ci-${os}"], "generator": "Ninja", "environment": { @@ -130,7 +158,7 @@ "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug", "CMAKE_EXPORT_COMPILE_COMMANDS": "ON", - "CMAKE_CXX_FLAGS": "$env{CXX_FLAGS_DEV_LINUX} $env{LOB_CXX_FLAGS_COMMON}", + "CMAKE_CXX_FLAGS": "\$env{CXX_FLAGS_DEV_LINUX} \$env{LOB_CXX_FLAGS_COMMON}", "CMAKE_LINKER_TYPE": "MOLD" } } @@ -161,11 +189,24 @@ EOF ) - if [ ! -f ${filename} ]; then - echo "$json" > ${filename} - echo "${filename} created successfully" + clangd=$(cat <<-EOF + CompileFlags: + CompilationDatabase: build/dev + EOF + ) + + if [ ! -f ${CMakeUserPresetsFile} ]; then + echo "$json" > ${CMakeUserPresetsFile} + echo "${CMakeUserPresetsFile} created successfully" + else + echo "${CMakeUserPresetsFile} already exists" + fi + + if [ ! -f ${clangdFile} ]; then + echo "$clangd" > ${clangdFile} + echo "${clangdFile} created successfully" else - echo "${filename} already exists" + echo "${clangdFile} already exists" fi '' + oldAttrs.shellHook; diff --git a/include/lob/lob.hpp b/include/lob/lob.hpp index 699427a..b473268 100644 --- a/include/lob/lob.hpp +++ b/include/lob/lob.hpp @@ -62,27 +62,31 @@ enum class LOB_EXPORT ClockAngleT : uint8_t { enum class LOB_EXPORT ErrorT : uint8_t { kNone, - kAirPressure, - kAltitude, - kAzimuth, - kBallisticCoefficient, - kBaseDiameter, - kDiameter, - kHumidity, - kInitialVelocity, - kLatitude, - kLength, - kMachDragTable, - kMass, - kMaximumTime, - kMeplatDiameter, - kNoseLength, - kOgiveRtR, - kRangeAngle, - kTailLength, - kWindHeading, - kZeroAngle, - kZeroDistance, + kAirPressureOOR, + kAltitudeOfBarometerOOR, + kAltitudeOfFiringSiteOOR, + kAltitudeOfThermometerOOR, + kAzimuthOOR, + kBallisticCoefficientOOR, + kBallisticCoefficientRequired, + kBaseDiameterOOR, + kDiameterOOR, + kHumidityOOR, + kInitialVelocityRequired, + kInternalError, + kLatitudeOOR, + kLengthOOR, + kMassOOR, + kMaximumTimeOOR, + kMeplatDiameterOOR, + kNoseLengthOOR, + kOgiveRtROOR, + kRangeAngleOOR, + kTailLengthOOR, + kWindHeadingOOR, + kZeroAngleOOR, + kZeroDataRequired, + kZeroDistanceOOR, kNotFormed }; // enum class ErrorT @@ -437,12 +441,6 @@ class LOB_EXPORT Builder { */ Builder& Reset() noexcept; - /** - * @brief Checks if the current builder state is well-formed. - * @return True if state is valid, false otherwise. - */ - bool IsValid() const; - /** * @brief Builds the `Input` object with the configured parameters. * @return The constructed `Input` object. diff --git a/source/lob_builder.cpp b/source/lob_builder.cpp index 51bd57b..d40e023 100644 --- a/source/lob_builder.cpp +++ b/source/lob_builder.cpp @@ -100,67 +100,36 @@ Builder& Builder::operator=(Builder&& rhs) noexcept { } Builder& Builder::AltitudeOfFiringSiteFt(double value) { - const bool kIsValid = (-kIsaStratosphereAltitudeFt < value) && - (value < kIsaStratosphereAltitudeFt); - if (!kIsValid) { - pimpl_->build.error = ErrorT::kAltitude; - } pimpl_->altitude_ft = FeetT(value); return *this; } Builder& Builder::AltitudeOfBarometerFt(double value) { - const bool kIsValid = (-kIsaStratosphereAltitudeFt < value) && - (value < kIsaStratosphereAltitudeFt); - if (!kIsValid) { - pimpl_->build.error = ErrorT::kAltitude; - } pimpl_->altitude_of_barometer_ft = FeetT(value); return *this; } Builder& Builder::AltitudeOfThermometerFt(double value) { - const bool kIsValid = (-kIsaStratosphereAltitudeFt < value) && - (value < kIsaStratosphereAltitudeFt); - if (!kIsValid) { - pimpl_->build.error = ErrorT::kAltitude; - } pimpl_->altitude_of_thermometer_ft = FeetT(value); return *this; } Builder& Builder::AzimuthDeg(double value) { - const bool kIsValid = (-kDegreesPerTurn < value) && (value < kDegreesPerTurn); - if (!kIsValid) { - pimpl_->build.error = ErrorT::kAzimuth; - } pimpl_->azimuth_rad = DegreesT(value); return *this; } Builder& Builder::BallisticCoefficientPsi(double value) { - const bool kIsValid = (0.0 < value); - if (!kIsValid) { - pimpl_->build.error = ErrorT::kBallisticCoefficient; - } pimpl_->ballistic_coefficient_psi = PmsiT(value); return *this; } Builder& Builder::AirPressureInHg(double value) { - const bool kIsValid = (0.0 < value); - if (!kIsValid) { - pimpl_->build.error = ErrorT::kAirPressure; - } pimpl_->air_pressure_in_hg = InHgT(value); return *this; } Builder& Builder::BaseDiameterInch(double value) { - const bool kIsValid = (0.0 <= value); - if (!kIsValid) { - pimpl_->build.error = ErrorT::kBaseDiameter; - } pimpl_->base_diameter_in = InchT(value); return *this; } @@ -204,37 +173,21 @@ Builder& Builder::BCDragFunction(DragFunctionT type) { } Builder& Builder::DiameterInch(double value) { - const bool kIsValid = (0.0 < value); - if (!kIsValid) { - pimpl_->build.error = ErrorT::kDiameter; - } pimpl_->diameter_in = InchT(value); return *this; } Builder& Builder::InitialVelocityFps(uint16_t value) { - const bool kIsValid = (0 != value); - if (!kIsValid) { - pimpl_->build.error = ErrorT::kInitialVelocity; - } pimpl_->build.velocity = FpsT(value).U16(); return *this; } Builder& Builder::LatitudeDeg(double value) { - const bool kIsValid = (-90.0 <= value && value <= 90.0); - if (!kIsValid) { - pimpl_->build.error = ErrorT::kLatitude; - } pimpl_->latitude_rad = DegreesT(value); return *this; } Builder& Builder::LengthInch(double value) { - const bool kIsValid = (0 < value); - if (!kIsValid) { - pimpl_->build.error = ErrorT::kLength; - } pimpl_->length_in = InchT(value); return *this; } @@ -242,7 +195,6 @@ Builder& Builder::LengthInch(double value) { Builder& Builder::MachVsDragTable(const float* pmachs, const float* pdrags, size_t size) { if (pmachs == nullptr || pdrags == nullptr || size == 0) { - pimpl_->build.error = ErrorT::kMachDragTable; return *this; } for (size_t i = 0; i < kTableSize; i++) { @@ -257,28 +209,16 @@ Builder& Builder::MachVsDragTable(const float* pmachs, const float* pdrags, } Builder& Builder::MassGrains(double value) { - const bool kIsValid = (0.0 < value); - if (!kIsValid) { - pimpl_->build.error = ErrorT::kMass; - } pimpl_->build.mass = LbsT(GrainT(value)).Value(); return *this; } Builder& Builder::MaximumTime(double value) { - const bool kIsValid = (0.0 < value); - if (!kIsValid) { - pimpl_->build.error = ErrorT::kMaximumTime; - } pimpl_->build.max_time = value; return *this; } Builder& Builder::MeplatDiameterInch(double value) { - const bool kIsValid = (0.0 <= value); - if (!kIsValid) { - pimpl_->build.error = ErrorT::kMeplatDiameter; - } pimpl_->meplat_diameter_in = InchT(value); return *this; } @@ -294,19 +234,11 @@ Builder& Builder::MinimumSpeed(uint16_t value) { } Builder& Builder::NoseLengthInch(double value) { - const bool kIsValid = (0.0 <= value); - if (!kIsValid) { - pimpl_->build.error = ErrorT::kNoseLength; - } pimpl_->nose_length_in = InchT(value); return *this; } Builder& Builder::OgiveRtR(double value) { - const bool kIsValid = (0.0 <= value && value <= 1.0); - if (!kIsValid) { - pimpl_->build.error = ErrorT::kOgiveRtR; - } pimpl_->ogive_rtr = value; return *this; } @@ -317,19 +249,11 @@ Builder& Builder::OpticHeightInches(double value) { } Builder& Builder::RelativeHumidityPercent(double value) { - const bool kIsValid = (0.0 <= value); - if (!kIsValid) { - pimpl_->build.error = ErrorT::kHumidity; - } pimpl_->relative_humidity_percent = value; return *this; } Builder& Builder::RangeAngleDeg(double value) { - const bool kIsValid = (-90.0 < value && value < 90.0); - if (!kIsValid) { - pimpl_->build.error = ErrorT::kRangeAngle; - } pimpl_->range_angle_rad = RadiansT(DegreesT(value)); return *this; } @@ -340,10 +264,6 @@ Builder& Builder::StepSize(uint16_t value) { } Builder& Builder::TailLengthInch(double value) { - const bool kIsValid = (0.0 <= value); - if (!kIsValid) { - pimpl_->build.error = ErrorT::kTailLength; - } pimpl_->tail_length_in = InchT(value); return *this; } @@ -374,10 +294,6 @@ Builder& Builder::WindHeadingDeg(double value) { const DegreesT kFullTurn(kDegreesPerTurn); const DegreesT kQuarterTurn(kFullTurn / 4); DegreesT angle(value); - const bool kIsValid = (kFullTurn * -1 < angle && angle < kFullTurn); - if (!kIsValid) { - pimpl_->build.error = ErrorT::kWindHeading; - } angle = angle * -1 + kQuarterTurn; @@ -401,19 +317,11 @@ Builder& Builder::WindSpeedMph(double value) { } Builder& Builder::ZeroAngleMOA(double value) { - const bool kIsInvalid = (-45.0 > value || value > 45.0); - if (kIsInvalid) { - pimpl_->build.error = ErrorT::kZeroAngle; - } pimpl_->build.zero_angle = MoaT(value).Value(); return *this; } Builder& Builder::ZeroDistanceYds(double value) { - const bool kIsValid = (0.0 < value); - if (!kIsValid) { - pimpl_->build.error = ErrorT::kZeroDistance; - } pimpl_->zero_distance_ft = YardT(value); return *this; } @@ -429,18 +337,8 @@ Builder& Builder::Reset() noexcept { return *this; } -bool Builder::IsValid() const { - const bool kErrorIsInExpectedState = - pimpl_->build.error == ErrorT::kNotFormed || - pimpl_->build.error == ErrorT::kNone; - const bool kBCisOk = !pimpl_->ballistic_coefficient_psi.IsNaN(); - const bool kVelocityIsOk = pimpl_->build.velocity > 0; - const bool kZeroIsOk = !pimpl_->zero_distance_ft.IsNaN() || - !std::isnan(pimpl_->build.zero_angle); - return kErrorIsInExpectedState && kBCisOk && kVelocityIsOk && kZeroIsOk; -} - namespace { + void BuildEnvironment(Impl* pimpl) { assert(pimpl != nullptr); FeetT altitude_of_firing_site = FeetT(0); @@ -454,6 +352,13 @@ void BuildEnvironment(Impl* pimpl) { pimpl->range_angle_rad = RadiansT(DegreesT(0)); } + const bool kRangeAngleValid = pimpl->range_angle_rad > DegreesT(-90.0) && + pimpl->range_angle_rad < DegreesT(90.0); + if (!kRangeAngleValid) { + pimpl->build.error = ErrorT::kRangeAngleOOR; + return; + } + pimpl->build.gravity.x = kStandardGravityFtPerSecSq * -1 * std::sin(pimpl->range_angle_rad.Value()); pimpl->build.gravity.y = kStandardGravityFtPerSecSq * -1 * @@ -468,6 +373,26 @@ void BuildEnvironment(Impl* pimpl) { ? pimpl->altitude_ft : pimpl->altitude_of_thermometer_ft; + auto is_altitude_valid = [](FeetT altitude) -> bool { + return FeetT(-kIsaStratosphereAltitudeFt) < altitude && + altitude < FeetT(kIsaStratosphereAltitudeFt); + }; + + if (!is_altitude_valid(altitude_of_firing_site)) { + pimpl->build.error = ErrorT::kAltitudeOfFiringSiteOOR; + return; + } + + if (!is_altitude_valid(altitude_of_barometer)) { + pimpl->build.error = ErrorT::kAltitudeOfBarometerOOR; + return; + } + + if (!is_altitude_valid(altitude_of_thermometer)) { + pimpl->build.error = ErrorT::kAltitudeOfThermometerOOR; + return; + } + temperature_at_firing_site = CalculateTemperatureAtAltitude( altitude_of_firing_site, DegFT(kIsaSeaLevelDegF)); pressure_at_firing_site = BarometricFormula(altitude_of_firing_site, @@ -485,6 +410,10 @@ void BuildEnvironment(Impl* pimpl) { } if (!std::isnan(pimpl->air_pressure_in_hg)) { + if (pimpl->air_pressure_in_hg < InHgT(0.0)) { + pimpl->build.error = ErrorT::kAirPressureOOR; + return; + } pressure_at_firing_site = BarometricFormula(altitude_of_firing_site - altitude_of_barometer, pimpl->air_pressure_in_hg, temperature_at_barometer); @@ -494,6 +423,11 @@ void BuildEnvironment(Impl* pimpl) { pimpl->relative_humidity_percent = kIsaSeaLevelHumidityPercent; } + if (pimpl->relative_humidity_percent < 0.0) { + pimpl->build.error = ErrorT::kHumidityOOR; + return; + } + const auto kWaterVaporSaturationPressureInHg = CalculateWaterVaporSaturationPressure(temperature_at_firing_site); @@ -514,9 +448,18 @@ void BuildEnvironment(Impl* pimpl) { void BuildTable(Impl* pimpl) { assert(pimpl != nullptr); - assert(!std::isnan(pimpl->ballistic_coefficient_psi)); assert(!std::isnan(pimpl->air_density_lbs_per_cu_ft)); + if (pimpl->ballistic_coefficient_psi.IsNaN()) { + pimpl->build.error = ErrorT::kBallisticCoefficientRequired; + return; + } + + if (pimpl->ballistic_coefficient_psi <= PmsiT(0.0)) { + pimpl->build.error = ErrorT::kBallisticCoefficientOOR; + return; + } + if (pimpl->atmosphere_reference == AtmosphereReferenceT::kArmyStandardMetro) { pimpl->ballistic_coefficient_psi *= kArmyToIcaoBcConversionFactor; pimpl->atmosphere_reference = AtmosphereReferenceT::kIcao; @@ -535,10 +478,18 @@ void BuildTable(Impl* pimpl) { void BuildWind(Impl* pimpl) { assert(pimpl != nullptr); + if (std::isnan(pimpl->wind_heading_rad)) { pimpl->wind_heading_rad = DegreesT(0); } + const DegreesT kFullTurn(kDegreesPerTurn); + if (pimpl->wind_heading_rad > kFullTurn || + pimpl->wind_heading_rad < kFullTurn * -1) { + pimpl->build.error = ErrorT::kWindHeadingOOR; + return; + } + if (std::isnan(pimpl->wind_speed_fps)) { pimpl->wind_speed_fps = FpsT(0); } @@ -551,28 +502,82 @@ void BuildWind(Impl* pimpl) { .Value(); } +void BuildOpticHeight(Impl* pimpl) { + assert(pimpl != nullptr); + if (std::isnan(pimpl->build.optic_height)) { + constexpr FeetT kDefaultOpticHeight = InchT(1.5); + pimpl->build.optic_height = kDefaultOpticHeight.Value(); + } +} + void BuildStability(Impl* pimpl) { assert(pimpl != nullptr); - assert(pimpl->build.velocity > 0); assert(!std::isnan(pimpl->air_density_lbs_per_cu_ft)); - if ((pimpl->diameter_in > InchT(0)) && (pimpl->length_in > InchT(0)) && - !AreEqual(pimpl->twist_inches_per_turn, InchPerTwistT(0)) && - !std::isnan(pimpl->build.mass)) { - const double kFtp = CalculateMillerTwistRuleCorrectionFactor( - pimpl->air_density_lbs_per_cu_ft); - pimpl->build.stability_factor = - kFtp * CalculateMillerTwistRuleStabilityFactor( - pimpl->diameter_in, GrainT(LbsT(pimpl->build.mass)), - pimpl->length_in, pimpl->twist_inches_per_turn, - FpsT(pimpl->build.velocity)); + if (pimpl->build.velocity == 0) { + pimpl->build.error = ErrorT::kInitialVelocityRequired; + return; + } + + if (pimpl->diameter_in <= InchT(0)) { + pimpl->build.error = ErrorT::kDiameterOOR; + return; + } + + if (pimpl->length_in <= InchT(0)) { + pimpl->build.error = ErrorT::kLengthOOR; + return; + } + + if (pimpl->build.mass <= 0) { + pimpl->build.error = ErrorT::kMassOOR; + return; + } + + if (pimpl->diameter_in.IsNaN() || pimpl->length_in.IsNaN() || + std::isnan(pimpl->build.mass) || pimpl->twist_inches_per_turn.IsNaN() || + AreEqual(pimpl->twist_inches_per_turn, InchPerTwistT(0))) { + return; } + + const double kFtp = CalculateMillerTwistRuleCorrectionFactor( + pimpl->air_density_lbs_per_cu_ft); + pimpl->build.stability_factor = + kFtp * CalculateMillerTwistRuleStabilityFactor( + pimpl->diameter_in, GrainT(LbsT(pimpl->build.mass)), + pimpl->length_in, pimpl->twist_inches_per_turn, + FpsT(pimpl->build.velocity)); } void BuildBoatright(Impl* pimpl) { assert(pimpl != nullptr); assert(pimpl->pdrag_lut != nullptr); + if (pimpl->meplat_diameter_in < InchT(0)) { + pimpl->build.error = ErrorT::kMeplatDiameterOOR; + return; + } + + if (pimpl->base_diameter_in <= InchT(0)) { + pimpl->build.error = ErrorT::kBaseDiameterOOR; + return; + } + + if (pimpl->nose_length_in < InchT(0)) { + pimpl->build.error = ErrorT::kNoseLengthOOR; + return; + } + + if (pimpl->tail_length_in < InchT(0)) { + pimpl->build.error = ErrorT::kTailLengthOOR; + return; + } + + if (pimpl->ogive_rtr < 0 || pimpl->ogive_rtr > 1.0) { + pimpl->build.error = ErrorT::kOgiveRtROOR; + return; + } + const InchT kD(pimpl->diameter_in); const CaliberT kDM(pimpl->meplat_diameter_in, kD.Inverse()); const CaliberT kDB(pimpl->base_diameter_in, kD.Inverse()); @@ -698,7 +703,20 @@ void BuildLitzAerodynamicJump(Impl* pimpl) { void BuildCoriolis(Impl* pimpl) { assert(pimpl != nullptr); + if (!std::isnan(pimpl->azimuth_rad) && !std::isnan(pimpl->latitude_rad)) { + const DegreesT kAzimuthLimit(kDegreesPerTurn); + if (pimpl->azimuth_rad > kAzimuthLimit || + pimpl->azimuth_rad < kAzimuthLimit * -1) { + pimpl->build.error = ErrorT::kAzimuthOOR; + return; + } + const DegreesT kLatitudeLimit(90); + if (pimpl->latitude_rad > kLatitudeLimit || + pimpl->latitude_rad < kLatitudeLimit * -1) { + pimpl->build.error = ErrorT::kLatitudeOOR; + return; + } // Coriolis Effect Page 178 of Modern Exterior Ballistics - McCoy const double kCosL = std::cos(pimpl->latitude_rad).Value(); const double kSinA = std::sin(pimpl->azimuth_rad).Value(); @@ -719,11 +737,26 @@ void BuildCoriolis(Impl* pimpl) { void BuildZeroAngle(Impl* pimpl) { assert(pimpl != nullptr); + if (!std::isnan(pimpl->build.zero_angle)) { + const double kZeroAngleLimit = MoaT(DegreesT(45)).Value(); + if (pimpl->build.zero_angle > kZeroAngleLimit || + pimpl->build.zero_angle < kZeroAngleLimit * -1) { + pimpl->build.error = ErrorT::kZeroAngleOOR; + } + return; + } + + if (pimpl->zero_distance_ft.IsNaN()) { + pimpl->build.error = ErrorT::kZeroDataRequired; + return; + } + + if (pimpl->zero_distance_ft <= FeetT(0)) { + pimpl->build.error = ErrorT::kZeroDistanceOOR; return; } - assert(!std::isnan(pimpl->zero_distance_ft)); assert(pimpl->build.velocity > 0); assert(!std::isnan(pimpl->build.aerodynamic_jump)); @@ -772,6 +805,12 @@ void BuildZeroAngle(Impl* pimpl) { void BuildOptions(Impl* pimpl) { assert(pimpl != nullptr); + + if (pimpl->build.max_time <= 0.0) { + pimpl->build.error = ErrorT::kMaximumTimeOOR; + return; + } + const FpsT kMinSpeed = CalculateVelocityFromKineticEnergy( pimpl->minimum_energy_ft_lbs, SlugT(LbsT(pimpl->build.mass))); pimpl->build.minimum_speed = @@ -781,24 +820,41 @@ void BuildOptions(Impl* pimpl) { } // namespace Input Builder::Build() { - if (pimpl_->build.error == ErrorT::kNone) { - pimpl_->build.error = ErrorT::kNotFormed; - } - if (IsValid()) { - // This order matters - BuildEnvironment(pimpl_); - BuildTable(pimpl_); - BuildWind(pimpl_); - if (std::isnan(pimpl_->build.optic_height)) { - constexpr FeetT kDefaultOpticHeight = InchT(1.5); - pimpl_->build.optic_height = kDefaultOpticHeight.Value(); - } - BuildStability(pimpl_); - BuildBoatright(pimpl_); - BuildLitzAerodynamicJump(pimpl_); - BuildCoriolis(pimpl_); - BuildZeroAngle(pimpl_); - BuildOptions(pimpl_); + pimpl_->build.error = ErrorT::kNotFormed; + // This order matters + BuildEnvironment(pimpl_); + if (pimpl_->build.error != ErrorT::kNotFormed) { + return pimpl_->build; + } + BuildTable(pimpl_); + if (pimpl_->build.error != ErrorT::kNotFormed) { + return pimpl_->build; + } + BuildWind(pimpl_); + if (pimpl_->build.error != ErrorT::kNotFormed) { + return pimpl_->build; + } + BuildOpticHeight(pimpl_); + BuildStability(pimpl_); + if (pimpl_->build.error != ErrorT::kNotFormed) { + return pimpl_->build; + } + BuildBoatright(pimpl_); + if (pimpl_->build.error != ErrorT::kNotFormed) { + return pimpl_->build; + } + BuildLitzAerodynamicJump(pimpl_); + BuildCoriolis(pimpl_); + if (pimpl_->build.error != ErrorT::kNotFormed) { + return pimpl_->build; + } + BuildZeroAngle(pimpl_); + if (pimpl_->build.error != ErrorT::kNotFormed) { + return pimpl_->build; + } + BuildOptions(pimpl_); + + if (pimpl_->build.error == ErrorT::kNotFormed) { pimpl_->build.error = ErrorT::kNone; } return pimpl_->build; diff --git a/test/source/lob_builder_test.cpp b/test/source/lob_builder_test.cpp index 6f2b036..29fdc12 100644 --- a/test/source/lob_builder_test.cpp +++ b/test/source/lob_builder_test.cpp @@ -129,31 +129,31 @@ TEST_F(BuilderTestFixture, BuildMinimalInput) { EXPECT_DOUBLE_EQ(kResult.gravity.y, -1.0 * lob::kStandardGravityFtPerSecSq); } -TEST_F(BuilderTestFixture, BuildInvalidVelocityInput) { +TEST_F(BuilderTestFixture, BuildMissingVelocityInput) { const double kTestBC = 0.425; const double kTestZeroAngle = 3.84; const lob::Input kResult = puut->BallisticCoefficientPsi(kTestBC) .ZeroAngleMOA(kTestZeroAngle) .Build(); - EXPECT_TRUE(std::isnan(kResult.table_coefficient)); + EXPECT_EQ(kResult.error, lob::ErrorT::kInitialVelocityRequired); } -TEST_F(BuilderTestFixture, BuildInvalidBCInput) { +TEST_F(BuilderTestFixture, BuildMissingBCInput) { const uint16_t kTestMuzzleVelocity = 2700U; const double kTestZeroAngle = 3.84; const lob::Input kResult = puut->InitialVelocityFps(kTestMuzzleVelocity) .ZeroAngleMOA(kTestZeroAngle) .Build(); - EXPECT_TRUE(std::isnan(kResult.table_coefficient)); + EXPECT_EQ(kResult.error, lob::ErrorT::kBallisticCoefficientRequired); } -TEST_F(BuilderTestFixture, BuildInvalidZeroInput) { +TEST_F(BuilderTestFixture, BuildMissingZeroInput) { const double kTestBC = 0.425; const uint16_t kTestMuzzleVelocity = 2700U; const lob::Input kResult = puut->BallisticCoefficientPsi(kTestBC) .InitialVelocityFps(kTestMuzzleVelocity) .Build(); - EXPECT_TRUE(std::isnan(kResult.table_coefficient)); + EXPECT_EQ(kResult.error, lob::ErrorT::kZeroDataRequired); } TEST_F(BuilderTestFixture, BuildG1UsingCustomTable) { @@ -358,9 +358,10 @@ TEST_F(BuilderTestFixture, ResetWorks) { .InitialVelocityFps(kM70MuzzleVelocity) .ZeroDistanceYds(kZeroYardage) .ZeroImpactHeightInches(kZeroHeight) - .Reset() .Build(); - EXPECT_TRUE(kJack.error == lob::ErrorT::kNotFormed); + EXPECT_EQ(kJack.error, lob::ErrorT::kNone); + const lob::Input kReset = puut->Reset().Build(); + EXPECT_TRUE(kReset.error != lob::ErrorT::kNone); } TEST_F(BuilderTestFixture, AirPressureError) { @@ -376,7 +377,7 @@ TEST_F(BuilderTestFixture, AirPressureError) { .ZeroImpactHeightInches(kZeroHeight) .AirPressureInHg(-1.0) .Build(); - EXPECT_TRUE(kJack.error == lob::ErrorT::kAirPressure); + EXPECT_EQ(kJack.error, lob::ErrorT::kAirPressureOOR); } TEST_F(BuilderTestFixture, FiringSiteAltitudeError) { @@ -392,12 +393,13 @@ TEST_F(BuilderTestFixture, FiringSiteAltitudeError) { .ZeroImpactHeightInches(kZeroHeight) .AltitudeOfFiringSiteFt(lob::kIsaStratosphereAltitudeFt + 1) .Build(); - EXPECT_TRUE(kJack.error == lob::ErrorT::kAltitude); + EXPECT_EQ(kJack.error, lob::ErrorT::kAltitudeOfFiringSiteOOR); } TEST_F(BuilderTestFixture, BarometerAltitudeError) { const double kSierraGameKingBC = 0.436; const uint16_t kM70MuzzleVelocity = 3100U; + const double kAltitude = 0.0; const double kZeroYardage = 100.0; const double kZeroHeight = 3.0; const lob::Input kJack = @@ -406,14 +408,16 @@ TEST_F(BuilderTestFixture, BarometerAltitudeError) { .InitialVelocityFps(kM70MuzzleVelocity) .ZeroDistanceYds(kZeroYardage) .ZeroImpactHeightInches(kZeroHeight) + .AltitudeOfFiringSiteFt(kAltitude) .AltitudeOfBarometerFt(lob::kIsaStratosphereAltitudeFt + 1) .Build(); - EXPECT_TRUE(kJack.error == lob::ErrorT::kAltitude); + EXPECT_EQ(kJack.error, lob::ErrorT::kAltitudeOfBarometerOOR); } TEST_F(BuilderTestFixture, ThermometerAltitudeError) { const double kSierraGameKingBC = 0.436; const uint16_t kM70MuzzleVelocity = 3100U; + const double kAltitude = 0.0; const double kZeroYardage = 100.0; const double kZeroHeight = 3.0; const lob::Input kJack = @@ -422,14 +426,16 @@ TEST_F(BuilderTestFixture, ThermometerAltitudeError) { .InitialVelocityFps(kM70MuzzleVelocity) .ZeroDistanceYds(kZeroYardage) .ZeroImpactHeightInches(kZeroHeight) + .AltitudeOfFiringSiteFt(kAltitude) .AltitudeOfThermometerFt(lob::kIsaStratosphereAltitudeFt + 1) .Build(); - EXPECT_TRUE(kJack.error == lob::ErrorT::kAltitude); + EXPECT_EQ(kJack.error, lob::ErrorT::kAltitudeOfThermometerOOR); } TEST_F(BuilderTestFixture, AzimuthError) { const double kSierraGameKingBC = 0.436; const uint16_t kM70MuzzleVelocity = 3100U; + const double kLatitude = 45.0; const double kZeroYardage = 100.0; const double kZeroHeight = 3.0; const lob::Input kJack = @@ -439,8 +445,9 @@ TEST_F(BuilderTestFixture, AzimuthError) { .ZeroDistanceYds(kZeroYardage) .ZeroImpactHeightInches(kZeroHeight) .AzimuthDeg(lob::kDegreesPerTurn + 1) + .LatitudeDeg(kLatitude) .Build(); - EXPECT_TRUE(kJack.error == lob::ErrorT::kAzimuth); + EXPECT_EQ(kJack.error, lob::ErrorT::kAzimuthOOR); } TEST_F(BuilderTestFixture, BallisticCoefficientError) { @@ -455,7 +462,7 @@ TEST_F(BuilderTestFixture, BallisticCoefficientError) { .ZeroDistanceYds(kZeroYardage) .ZeroImpactHeightInches(kZeroHeight) .Build(); - EXPECT_TRUE(kJack.error == lob::ErrorT::kBallisticCoefficient); + EXPECT_EQ(kJack.error, lob::ErrorT::kBallisticCoefficientOOR); } TEST_F(BuilderTestFixture, BaseDiameterError) { @@ -471,7 +478,7 @@ TEST_F(BuilderTestFixture, BaseDiameterError) { .ZeroImpactHeightInches(kZeroHeight) .BaseDiameterInch(-1.0) .Build(); - EXPECT_TRUE(kJack.error == lob::ErrorT::kBaseDiameter); + EXPECT_EQ(kJack.error, lob::ErrorT::kBaseDiameterOOR); } TEST_F(BuilderTestFixture, DiameterError) { @@ -487,7 +494,7 @@ TEST_F(BuilderTestFixture, DiameterError) { .ZeroImpactHeightInches(kZeroHeight) .DiameterInch(-1.0) .Build(); - EXPECT_TRUE(kJack.error == lob::ErrorT::kDiameter); + EXPECT_EQ(kJack.error, lob::ErrorT::kDiameterOOR); } TEST_F(BuilderTestFixture, HumidityError) { @@ -503,7 +510,7 @@ TEST_F(BuilderTestFixture, HumidityError) { .ZeroImpactHeightInches(kZeroHeight) .RelativeHumidityPercent(-1.0) .Build(); - EXPECT_TRUE(kJack.error == lob::ErrorT::kHumidity); + EXPECT_EQ(kJack.error, lob::ErrorT::kHumidityOOR); } TEST_F(BuilderTestFixture, InitialVelocity) { @@ -517,12 +524,13 @@ TEST_F(BuilderTestFixture, InitialVelocity) { .ZeroDistanceYds(kZeroYardage) .ZeroImpactHeightInches(kZeroHeight) .Build(); - EXPECT_TRUE(kJack.error == lob::ErrorT::kInitialVelocity); + EXPECT_EQ(kJack.error, lob::ErrorT::kInitialVelocityRequired); } TEST_F(BuilderTestFixture, LatitudeError) { const double kSierraGameKingBC = 0.436; const uint16_t kM70MuzzleVelocity = 3100U; + const double kAzimuth = 0; const double kZeroYardage = 100.0; const double kZeroHeight = 3.0; const lob::Input kJack = @@ -531,9 +539,10 @@ TEST_F(BuilderTestFixture, LatitudeError) { .InitialVelocityFps(kM70MuzzleVelocity) .ZeroDistanceYds(kZeroYardage) .ZeroImpactHeightInches(kZeroHeight) + .AzimuthDeg(kAzimuth) .LatitudeDeg(91.0) .Build(); - EXPECT_TRUE(kJack.error == lob::ErrorT::kLatitude); + EXPECT_EQ(kJack.error, lob::ErrorT::kLatitudeOOR); } TEST_F(BuilderTestFixture, LengthError) { @@ -549,23 +558,21 @@ TEST_F(BuilderTestFixture, LengthError) { .ZeroImpactHeightInches(kZeroHeight) .LengthInch(-1.0) .Build(); - EXPECT_TRUE(kJack.error == lob::ErrorT::kLength); + EXPECT_EQ(kJack.error, lob::ErrorT::kLengthOOR); } -TEST_F(BuilderTestFixture, MachVsDragTableError) { - const double kSierraGameKingBC = 0.436; +TEST_F(BuilderTestFixture, MachVsDragTableBadParamsIgnored) { const uint16_t kM70MuzzleVelocity = 3100U; const double kZeroYardage = 100.0; const double kZeroHeight = 3.0; const lob::Input kJack = - puut->BallisticCoefficientPsi(kSierraGameKingBC) - .BCAtmosphere(lob::AtmosphereReferenceT::kArmyStandardMetro) + puut->BCAtmosphere(lob::AtmosphereReferenceT::kArmyStandardMetro) .InitialVelocityFps(kM70MuzzleVelocity) .ZeroDistanceYds(kZeroYardage) .ZeroImpactHeightInches(kZeroHeight) .MachVsDragTable(nullptr, nullptr, 0) .Build(); - EXPECT_TRUE(kJack.error == lob::ErrorT::kMachDragTable); + EXPECT_EQ(kJack.error, lob::ErrorT::kBallisticCoefficientRequired); } TEST_F(BuilderTestFixture, MassError) { @@ -581,7 +588,7 @@ TEST_F(BuilderTestFixture, MassError) { .ZeroImpactHeightInches(kZeroHeight) .MassGrains(-1.0) .Build(); - EXPECT_TRUE(kJack.error == lob::ErrorT::kMass); + EXPECT_EQ(kJack.error, lob::ErrorT::kMassOOR); } TEST_F(BuilderTestFixture, MaximumTimeError) { @@ -597,7 +604,7 @@ TEST_F(BuilderTestFixture, MaximumTimeError) { .ZeroImpactHeightInches(kZeroHeight) .MaximumTime(-1.0) .Build(); - EXPECT_TRUE(kJack.error == lob::ErrorT::kMaximumTime); + EXPECT_EQ(kJack.error, lob::ErrorT::kMaximumTimeOOR); } TEST_F(BuilderTestFixture, MeplatDiameterError) { @@ -613,7 +620,7 @@ TEST_F(BuilderTestFixture, MeplatDiameterError) { .ZeroImpactHeightInches(kZeroHeight) .MeplatDiameterInch(-1.0) .Build(); - EXPECT_TRUE(kJack.error == lob::ErrorT::kMeplatDiameter); + EXPECT_EQ(kJack.error, lob::ErrorT::kMeplatDiameterOOR); } TEST_F(BuilderTestFixture, NoseLengthError) { @@ -629,7 +636,7 @@ TEST_F(BuilderTestFixture, NoseLengthError) { .ZeroImpactHeightInches(kZeroHeight) .NoseLengthInch(-1) .Build(); - EXPECT_TRUE(kJack.error == lob::ErrorT::kNoseLength); + EXPECT_EQ(kJack.error, lob::ErrorT::kNoseLengthOOR); } TEST_F(BuilderTestFixture, OgiveRtRError) { @@ -645,7 +652,7 @@ TEST_F(BuilderTestFixture, OgiveRtRError) { .ZeroImpactHeightInches(kZeroHeight) .OgiveRtR(-1) .Build(); - EXPECT_TRUE(kJack.error == lob::ErrorT::kOgiveRtR); + EXPECT_EQ(kJack.error, lob::ErrorT::kOgiveRtROOR); } TEST_F(BuilderTestFixture, RangeAngleError) { @@ -661,7 +668,7 @@ TEST_F(BuilderTestFixture, RangeAngleError) { .ZeroImpactHeightInches(kZeroHeight) .RangeAngleDeg(90) .Build(); - EXPECT_TRUE(kJack.error == lob::ErrorT::kRangeAngle); + EXPECT_EQ(kJack.error, lob::ErrorT::kRangeAngleOOR); } TEST_F(BuilderTestFixture, TailLengthError) { @@ -677,12 +684,13 @@ TEST_F(BuilderTestFixture, TailLengthError) { .ZeroImpactHeightInches(kZeroHeight) .TailLengthInch(-1.0) .Build(); - EXPECT_TRUE(kJack.error == lob::ErrorT::kTailLength); + EXPECT_EQ(kJack.error, lob::ErrorT::kTailLengthOOR); } TEST_F(BuilderTestFixture, WindHeadingError) { const double kSierraGameKingBC = 0.436; const uint16_t kM70MuzzleVelocity = 3100U; + const uint16_t kWindSpeed = 10; const double kZeroYardage = 100.0; const double kZeroHeight = 3.0; const lob::Input kJack = @@ -691,9 +699,10 @@ TEST_F(BuilderTestFixture, WindHeadingError) { .InitialVelocityFps(kM70MuzzleVelocity) .ZeroDistanceYds(kZeroYardage) .ZeroImpactHeightInches(kZeroHeight) - .WindHeadingDeg(lob::kDegreesPerTurn + 1) + .WindHeadingDeg(lob::kDegreesPerTurn * 3) + .WindSpeedMph(kWindSpeed) .Build(); - EXPECT_TRUE(kJack.error == lob::ErrorT::kWindHeading); + EXPECT_EQ(kJack.error, lob::ErrorT::kWindHeadingOOR); } TEST_F(BuilderTestFixture, ZeroAngleError) { @@ -707,9 +716,9 @@ TEST_F(BuilderTestFixture, ZeroAngleError) { .InitialVelocityFps(kM70MuzzleVelocity) .ZeroDistanceYds(kZeroYardage) .ZeroImpactHeightInches(kZeroHeight) - .ZeroAngleMOA(46.0) + .ZeroAngleMOA(lob::MoaT(lob::DegreesT(46)).Value()) .Build(); - EXPECT_TRUE(kJack.error == lob::ErrorT::kZeroAngle); + EXPECT_EQ(kJack.error, lob::ErrorT::kZeroAngleOOR); } TEST_F(BuilderTestFixture, ZeroDistanceError) { @@ -724,7 +733,7 @@ TEST_F(BuilderTestFixture, ZeroDistanceError) { .ZeroDistanceYds(-kZeroYardage) .ZeroImpactHeightInches(kZeroHeight) .Build(); - EXPECT_TRUE(kJack.error == lob::ErrorT::kZeroDistance); + EXPECT_EQ(kJack.error, lob::ErrorT::kZeroDistanceOOR); } TEST_F(BuilderTestFixture, RangeAngleDeg) {