diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..aa57227 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,18 @@ +name: Node.js Unittests + +on: + pull_request: + branches: + - '*' + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 16 + - run: npm ci + - run: npm test \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..03dee0f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "cSpell.words": [ + "atmo" + ] +} \ No newline at end of file diff --git a/__tests__/trajectory.test.ts b/__tests__/trajectory.test.ts index 6a4711d..cb1efe4 100644 --- a/__tests__/trajectory.test.ts +++ b/__tests__/trajectory.test.ts @@ -1,5 +1,5 @@ import {describe, expect, test} from '@jest/globals'; -import {Ammo, Atmo, DragModel, Table, Shot, TrajectoryCalc, UNew, Unit, Weapon, Wind, TrajFlag} from "../src/index.js"; +import {Ammo, Atmo, DragModel, Table, Shot, TrajectoryCalc, UNew, Unit, Weapon, Wind, TrajFlag, TrajectoryData} from "../src/index.js"; describe("TrajectoryCalc", () => { @@ -105,7 +105,7 @@ describe("TrajectoryCalc", () => { customAssertEqual(data.length, 11, 0.1, "Length") const test_data = [ - [data[0], 0, 2750, 2.463, 2820.6, -2, 0, 0, 0, 0, 880, Unit.MOA], + [data[0], 0, 2750.0, 2.463, 2820.6, -2, 0, 0, 0, 0, 880, Unit.MOA], [data[1], 100, 2351.2, 2.106, 2061, 0, 0, -0.6, -0.6, 0.118, 550, Unit.MOA], [data[5], 500, 1169.1, 1.047, 509.8, -87.9, -16.8, -19.5, -3.7, 0.857, 67, Unit.MOA], [data[10], 1000, 776.4, 0.695, 224.9, -823.9, -78.7, -87.5, -8.4, 2.495, 20, Unit.MOA] diff --git a/__tests__/v2/atmosphere.test.ts b/__tests__/v2/atmosphere.test.ts new file mode 100644 index 0000000..c14763a --- /dev/null +++ b/__tests__/v2/atmosphere.test.ts @@ -0,0 +1,44 @@ +import { Atmo, Temperature, Pressure, Velocity, UNew } from "../../src/v2"; + +describe('Atmo Class Tests', () => { + let standard: Atmo; + let highICAO: Atmo; + let highISA: Atmo; + let custom: Atmo; + + beforeEach(() => { + standard = Atmo.standard({}); + highICAO = Atmo.standard({altitude: UNew.Foot(10000)}); + highISA = Atmo.standard({altitude: UNew.Meter(1000)}); + custom = new Atmo({ + pressure: UNew.InHg(31), + temperature: UNew.Fahrenheit(30), + humidity: 0.5 + } + ); + }); + + test('Standard atmosphere properties', () => { + expect(standard.temperature.In(Temperature.Fahrenheit)).toBeCloseTo(59.0, 1); + expect(standard.pressure.In(Pressure.hPa)).toBeCloseTo(1013.25, 1); + expect(standard.densityImperial).toBeCloseTo(0.076474, 4); + }); + + test('High altitude properties (ICAO and ISA)', () => { + // Ref https://www.engineeringtoolbox.com/standard-atmosphere-d_604.html + expect(highICAO.temperature.In(Temperature.Fahrenheit)).toBeCloseTo(23.36, 1); + expect(highICAO.densityRatio).toBeCloseTo(0.7387, 3); + // Ref https://www.engineeringtoolbox.com/international-standard-atmosphere-d_985.html + expect(highISA.pressure.In(Pressure.hPa)).toBeCloseTo(899, 0); + expect(highISA.densityRatio).toBeCloseTo(0.9075, 4); + }); + + test('Mach calculations', () => { + // Ref https://www.omnicalculator.com/physics/speed-of-sound + expect(Atmo.machF(59)).toBeCloseTo(1116.15, 0); + expect(Atmo.machF(10)).toBeCloseTo(1062.11, 0); + expect(Atmo.machF(99)).toBeCloseTo(1158.39, 0); + expect(Atmo.machC(-20)).toBeCloseTo(318.94, 1); + expect(highISA.mach.In(Velocity.MPS)).toBeCloseTo(336.4, 1); + }); +}); diff --git a/__tests__/v2/computer.test.ts b/__tests__/v2/computer.test.ts new file mode 100644 index 0000000..60a6a82 --- /dev/null +++ b/__tests__/v2/computer.test.ts @@ -0,0 +1,538 @@ +import { expect, describe, test, beforeEach } from '@jest/globals'; +import Calculator, { Ammo, Wind, Atmo, DragModel, Table, UNew, Weapon, Shot, HitResult, setGlobalUsePowderSensitivity, getGlobalUsePowderSensitivity } from '../../src/v2'; + + +describe('TestComputer', () => { + + let baselineShot: Shot; + let baselineTrajectory: HitResult; + let calc: Calculator; + let range: number; + let step: number; + let dm: DragModel; + let weapon: Weapon; + let ammo: Ammo; + let atmo: Atmo; + + beforeEach(() => { + range = 1000; + step = 100; + dm = new DragModel({ bc: 0.22, dragTable: Table.G7, weight: 168, diameter: 0.308, length: 1.22 }); + ammo = new Ammo({ dm: dm, mv: UNew.FPS(2600) }); + weapon = new Weapon({ sightHeight: 4, twist: 12 }); + atmo = Atmo.icao({}); + calc = new Calculator(); + baselineShot = new Shot({ weapon: weapon, ammo: ammo, atmo: atmo }); + baselineTrajectory = calc.fire({ shot: baselineShot, trajectoryRange: range, trajectoryStep: step }); + }); + + // region Cant_angle + + test('cant_zero_elevation', () => { + // Create a copy of the baseline shot and apply the cant_angle + const cantedShot = new Shot( + { + ...baselineShot + // baselineShot.weapon, + // baselineShot.ammo, + // baselineShot.lookAngle, + // baselineShot.relativeAngle, + // baselineShot.cantAngle, + // baselineShot.atmo, + // baselineShot.winds + } + ); + cantedShot.cantAngle = UNew.Degree(90); + + // Fire the canted shot + const cantedTrajectory = calc.fire({ + shot: cantedShot, + trajectoryRange: range, + trajectoryStep: step + }); + + // Perform the assertion comparing height and windage adjustments + console.log(cantedTrajectory.trajectory[5].height.rawValue) + console.log(baselineShot.weapon.sightHeight.rawValue) + console.log(baselineTrajectory.trajectory[5].height.rawValue) + expect( + cantedTrajectory.trajectory[5].height.rawValue - baselineShot.weapon.sightHeight.rawValue + ).toBeCloseTo(baselineTrajectory.trajectory[5].height.rawValue, 1e-2); + + expect( + cantedTrajectory.trajectory[5].windage.rawValue + baselineShot.weapon.sightHeight.rawValue + ).toBeCloseTo(baselineTrajectory.trajectory[5].windage.rawValue, 1e-2); + }); + + + test("test_cant_positive_elevation", () => { + const canted = new Shot({ + weapon: new Weapon({ + sightHeight: weapon.sightHeight, + twist: 0, + zeroElevation: UNew.MIL(2), + }), + ammo: ammo, + cantAngle: UNew.Degree(90), + atmo: atmo, + }); + + const t = calc.fire({ shot: canted, trajectoryRange: range, trajectoryStep: step }); + + // Assert height difference with baseline + expect(t.trajectory[5] + .height.rawValue - weapon.sightHeight.rawValue) + .toBeCloseTo(baselineTrajectory.trajectory[5].height.rawValue, 2); + + // Assert windage at muzzle + expect(t.trajectory[0].windage.rawValue) + .toBeCloseTo(-weapon.sightHeight.rawValue); + + // Assert increasing windage down-range + expect(t.trajectory[5].windage.rawValue) + .toBeGreaterThan(t.trajectory[3].windage.rawValue); + }); + + // Cant_angle test with zero sight height and 90 degrees cant angle + test('cant_zero_sight_height', () => { + // Create a new shot with the same parameters but a cant angle of 90 degrees + const cantedShot = new Shot({ + weapon: new Weapon({ sightHeight: UNew.Inch(0), twist: weapon.twist }), + ammo: ammo, + atmo: atmo, + cantAngle: UNew.Degree(90) + }); + + const cantedTrajectory = calc.fire({ + shot: cantedShot, + trajectoryRange: range, + trajectoryStep: step + }); + + // Assert that height difference matches the baseline with adjusted sight height + expect( + cantedTrajectory.trajectory[5].height.rawValue - weapon.sightHeight.rawValue + ).toBeCloseTo(baselineTrajectory.trajectory[5].height.rawValue, 2); + + // Assert windage has no significant change + expect( + cantedTrajectory.trajectory[5].windage.rawValue + ).toBeCloseTo(baselineTrajectory.trajectory[5].windage.rawValue, 2); + }); + + // end region Cant_angle + + + // region Wind + // Wind from left should increase windage + test('wind_from_left', () => { + // Create a shot with wind coming from the left + const windFromLeft = new Wind({ + velocity: UNew.MPH(5), + directionFrom: UNew.OClock(3) // Wind coming from the left (3 o'clock) + }); + + const shotWithWind = new Shot({ + weapon: weapon, + ammo: ammo, + atmo: atmo, + winds: [windFromLeft] + }); + + const trajectoryWithWind = calc.fire({ + shot: shotWithWind, + trajectoryRange: range, + trajectoryStep: step + }); + + // Assert that the windage is greater due to wind from the left + expect( + trajectoryWithWind + .trajectory[5].windage.rawValue) + .toBeGreaterThan(baselineTrajectory.trajectory[5].windage.rawValue); + }); + + // Wind from right should decrease windage + test('wind_from_right', () => { + // Create a shot with wind coming from the right + const windFromRight = new Wind({ + velocity: UNew.MPH(5), + directionFrom: UNew.OClock(9) // Wind coming from the right (9 o'clock) + }); + + const shotWithWind = new Shot({ + weapon: weapon, + ammo: ammo, + atmo: atmo, + winds: [windFromRight] + }); + + const trajectoryWithWind = calc.fire({ + shot: shotWithWind, + trajectoryRange: range, + trajectoryStep: step + }); + + // Assert that the windage is less due to wind from the right + expect( + trajectoryWithWind.trajectory[5].windage.rawValue + ).toBeLessThan(baselineTrajectory.trajectory[5].windage.rawValue); + }); + + // Wind from behind should decrease drop + test('wind_from_back', () => { + // Create a shot with wind coming from behind + const windFromBack = new Wind({ + velocity: UNew.MPH(5), + directionFrom: UNew.OClock(0) // Wind coming from behind (0 o'clock) + }); + + const shotWithWind = new Shot({ + weapon: weapon, + ammo: ammo, + atmo: atmo, + winds: [windFromBack] + }); + + const trajectoryWithWind = calc.fire({ + shot: shotWithWind, + trajectoryRange: range, + trajectoryStep: step + }); + + // Assert that the trajectory height is greater with wind from behind + expect( + trajectoryWithWind.trajectory[5].height.rawValue + ).toBeGreaterThan(baselineTrajectory.trajectory[5].height.rawValue); + }); + + // Wind from in front should increase drop + test('wind_from_front', () => { + // Create a shot with wind coming from the front + const windFromFront = new Wind({ + velocity: UNew.MPH(5), + directionFrom: UNew.OClock(6) // Wind coming from the front (6 o'clock) + }); + + const shotWithWind = new Shot({ + weapon: weapon, + ammo: ammo, + atmo: atmo, + winds: [windFromFront] + }); + + const trajectoryWithWind = calc.fire({ + shot: shotWithWind, + trajectoryRange: range, + trajectoryStep: step + }); + + // Assert that the trajectory height is less with wind from the front + expect( + trajectoryWithWind.trajectory[5].height.rawValue + ).toBeLessThan(baselineTrajectory.trajectory[5].height.rawValue); + }); + + // end region Wind + + // region Twist + test('no_twist', () => { + // Create a shot with no twist + const shotWithNoTwist = new Shot({ + weapon: new Weapon({ twist: 0 }), + ammo: ammo, + atmo: atmo + }); + + const trajectoryWithNoTwist = calc.fire({ + shot: shotWithNoTwist, + trajectoryRange: range, + trajectoryStep: step + }); + + // Assert that the windage is 0 with no twist + expect(trajectoryWithNoTwist.trajectory[5].windage.rawValue).toBe(0); + }); + + test('twist', () => { + // Create a shot with right-hand twist + const shotRightTwist = new Shot({ + weapon: new Weapon({ twist: 12 }), // Positive twist rate + ammo: ammo, + atmo: atmo + }); + + // Calculate trajectory for right-hand twist + const trajectoryRightTwist = calc.fire({ + shot: shotRightTwist, + trajectoryRange: range, + trajectoryStep: step + }); + + // Assert that windage is positive with right-hand twist + expect(trajectoryRightTwist.trajectory[5].windage.rawValue).toBeGreaterThan(0); + + // Create a shot with left-hand twist + const shotLeftTwist = new Shot({ + weapon: new Weapon({ twist: -8 }), // Negative twist rate + ammo: ammo, + atmo: atmo + }); + + // Calculate trajectory for left-hand twist + const trajectoryLeftTwist = calc.fire({ + shot: shotLeftTwist, + trajectoryRange: range, + trajectoryStep: step + }); + + // Assert that windage is negative with left-hand twist + expect(trajectoryLeftTwist.trajectory[5].windage.rawValue).toBeLessThan(0); + + // Assert that faster twist (right-hand twist) produces less drift compared to slower twist (left-hand twist) + expect( + -trajectoryLeftTwist.trajectory[5].windage.rawValue + ).toBeGreaterThan(trajectoryRightTwist.trajectory[5].windage.rawValue); + }); + + // end region Twist + + // region Atmo + + test('humidity', () => { + // Create an atmosphere with 90% humidity + const humidAtmo = new Atmo({ humidity: 0.9 }); + + // Create a shot with the humid atmosphere + const shotWithHumidity = new Shot({ + weapon: weapon, + ammo: ammo, + atmo: humidAtmo + }); + + // Calculate the trajectory for the shot with humidity + const trajectoryWithHumidity = calc.fire({ + shot: shotWithHumidity, + trajectoryRange: range, + trajectoryStep: step + }); + + // Assert that height is greater with increased humidity + expect(trajectoryWithHumidity.trajectory[5].height.rawValue).toBeGreaterThan( + baselineTrajectory.trajectory[5].height.rawValue + ); + }); + + test('temperature_atmo', () => { + // Create an atmosphere with temperature at 0°C + const coldAtmo = new Atmo({ temperature: UNew.Celsius(0) }); + + // Create a shot with the cold atmosphere + const shotInCold = new Shot({ + weapon: weapon, + ammo: ammo, + atmo: coldAtmo + }); + + // Calculate the trajectory for the shot in cold weather + const trajectoryInCold = calc.fire({ + shot: shotInCold, + trajectoryRange: range, + trajectoryStep: step + }); + + // Assert that the height is less in colder temperature, indicating increased drop + expect(trajectoryInCold.trajectory[5].height.rawValue).toBeLessThan( + baselineTrajectory.trajectory[5].height.rawValue + ); + }); + + test('altitude', () => { + // Create an atmosphere with altitude at 5000 feet + const highAtmo = Atmo.icao({ altitude: UNew.Foot(5000) }); + + // Create a shot with the high-altitude atmosphere + const shotAtHighAltitude = new Shot({ + weapon: weapon, + ammo: ammo, + atmo: highAtmo + }); + + // Calculate the trajectory for the shot at high altitude + const trajectoryAtHighAltitude = calc.fire({ + shot: shotAtHighAltitude, + trajectoryRange: range, + trajectoryStep: step + }); + + // Assert that the height is greater at higher altitude, indicating decreased drop + expect(trajectoryAtHighAltitude.trajectory[5].height.rawValue).toBeGreaterThan( + baselineTrajectory.trajectory[5].height.rawValue + ); + }); + + test('pressure', () => { + // Create an atmosphere with pressure at 20.0 inHg + const thinAtmo = new Atmo({ pressure: UNew.InHg(20.0) }); + + // Create a shot with the low-pressure atmosphere + const shotInLowPressure = new Shot({ + weapon: weapon, + ammo: ammo, + atmo: thinAtmo + }); + + // Calculate the trajectory for the shot in low pressure + const trajectoryInLowPressure = calc.fire({ + shot: shotInLowPressure, + trajectoryRange: range, + trajectoryStep: step + }); + + // Assert that the height is greater in lower pressure, indicating decreased drop + expect(trajectoryInLowPressure.trajectory[5].height.rawValue).toBeGreaterThan( + baselineTrajectory.trajectory[5].height.rawValue + ); + }); + + // end region Atmo + + // region Ammo + + test('ammo_drag', () => { + // Create a new DragModel with increased ballistic coefficient (bc) + const increasedDragModel = new DragModel({ + bc: dm.bc + 0.5, + dragTable: dm.dragTable, + weight: dm.weight, + diameter: dm.diameter, + length: dm.length + }); + + // Create new ammo with the updated DragModel + const slickAmmo = new Ammo({ + dm: increasedDragModel, + mv: ammo.mv + }); + + // Create a shot with the slick ammo + const shotWithSlickAmmo = new Shot({ + weapon: weapon, + ammo: slickAmmo, + atmo: atmo + }); + + // Calculate the trajectory for the shot with slick ammo + const trajectoryWithSlickAmmo = calc.fire({ + shot: shotWithSlickAmmo, + trajectoryRange: range, + trajectoryStep: step + }); + + // Assert that the height is greater with the increased ballistic coefficient, indicating decreased drop + expect(trajectoryWithSlickAmmo.trajectory[5].height.rawValue).toBeGreaterThan( + baselineTrajectory.trajectory[5].height.rawValue + ); + }); + + test('ammo_optional', () => { + // Create a new DragModel with only the ballistic coefficient + const reducedDragModel = new DragModel({ + bc: dm.bc, + dragTable: dm.dragTable + }); + + // Create new ammo with the reduced DragModel + const reducedAmmo = new Ammo({ + dm: reducedDragModel, + mv: ammo.mv + }); + + // Create a shot with the reduced ammo + const shotWithReducedAmmo = new Shot({ + weapon: weapon, + ammo: reducedAmmo, + atmo: atmo + }); + + // Calculate the trajectory for the shot with the reduced ammo + const trajectoryWithReducedAmmo = calc.fire({ + shot: shotWithReducedAmmo, + trajectoryRange: range, + trajectoryStep: step + }); + + // Assert that the height is the same as with the baseline, indicating no change in drop + expect(trajectoryWithReducedAmmo.trajectory[5].height.rawValue).toBeCloseTo( + baselineTrajectory.trajectory[5].height.rawValue, 1e-2 + ); + }); + + test('powder_sensitivity', () => { + // Store the previous global powder sensitivity setting + const previous = getGlobalUsePowderSensitivity(); + + // Set global powder sensitivity to true + setGlobalUsePowderSensitivity(true); + + // Adjust the ammo's powder sensitivity + ammo.calcPowderSens(UNew.FPS(2550), UNew.Celsius(0)); + + // Create a new atmosphere with reduced temperature + const coldAtmo = new Atmo({ + temperature: UNew.Celsius(-5) + }); + + // Create a new shot with the cold atmosphere + const coldShot = new Shot({ + weapon: weapon, + ammo: ammo, + atmo: coldAtmo + }); + + // Calculate the trajectory for the shot + const trajectoryWithCold = calc.fire({ + shot: coldShot, + trajectoryRange: range, + trajectoryStep: step + }); + + // Assert that the velocity at index 0 is less than the baseline velocity + expect(trajectoryWithCold.trajectory[0].velocity.rawValue).toBeLessThan( + baselineTrajectory.trajectory[0].velocity.rawValue + ); + + // Restore the previous global powder sensitivity setting + setGlobalUsePowderSensitivity(previous); + }); + + // end region Ammo + + // region Shot + test('winds_sort', () => { + // Create an array of Wind instances with varying distances + const winds = [ + new Wind({velocity: UNew.MPS(0), directionFrom: UNew.Degree(90), untilDistance: UNew.Meter(100)}), + new Wind({velocity: UNew.MPS(1), directionFrom: UNew.Degree(60), untilDistance: UNew.Meter(300)}), + new Wind({velocity: UNew.MPS(2), directionFrom: UNew.Degree(30), untilDistance: UNew.Meter(200)}), + new Wind({velocity: UNew.MPS(2), directionFrom: UNew.Degree(30), untilDistance: UNew.Meter(50)}) + ]; + + // Create a Shot instance with the winds array + const shot = new Shot({ + weapon: null, + ammo: null, + lookAngle: 0, + relativeAngle: 0, + cantAngle: 0, + winds: winds + }); + + // Assert the order of the sorted winds + expect(shot.winds[0]).toBe(winds[3]); + expect(shot.winds[1]).toBe(winds[0]); + expect(shot.winds[2]).toBe(winds[2]); + expect(shot.winds[3]).toBe(winds[1]); + }); + // end region Shot +}); \ No newline at end of file diff --git a/__tests__/v2/danger_space.test.ts b/__tests__/v2/danger_space.test.ts new file mode 100644 index 0000000..48ecc62 --- /dev/null +++ b/__tests__/v2/danger_space.test.ts @@ -0,0 +1,67 @@ +import Calculator, { HitResult, Ammo, UNew, DragModel, Shot, Table, Distance, Weapon, Wind } from '../../src/v2'; +import { expect, describe, test, beforeEach } from '@jest/globals'; + +describe('TestDangerSpace', () => { + let shotResult: HitResult; // Replace 'any' with the appropriate type if known + let lookAngle = UNew.Degree(0) + + beforeEach(() => { + // Initialize the look angle + + // Create the DragModel + const weight = 168; + const diameter = 0.308; + const length = UNew.Inch(1.282); + const dm = new DragModel({bc: 0.223, dragTable: Table.G7, weight: weight, diameter: diameter, length: length}); + + // Create Ammo and calculate powder sensitivity + const ammo = new Ammo({dm: dm, mv: UNew.FPS(2750), powderTemp: UNew.Celsius(15)}); + ammo.calcPowderSens(2723, 0); + + // Create current winds + const currentWinds = [new Wind({velocity: 2, directionFrom: UNew.Degree(90)})]; + + // Create Shot and Calculator + const shot = new Shot({ + weapon: new Weapon({}), + ammo: ammo, + winds: currentWinds + }); + + const calc = new Calculator(); + calc.setWeaponZero(shot, UNew.Foot(300)); + + // Fire the shot and store the result + shotResult = calc.fire({shot: shot, trajectoryRange: UNew.Yard(1000), trajectoryStep: UNew.Yard(100), extraData: true}); + }); + + test('danger_space', () => { + // First test + let dangerSpace = shotResult.dangerSpace( + UNew.Yard(500), + UNew.Meter(1.5), + lookAngle + ); + + // function assertAlmostEqual(actual: number, expected: number, tolerance: number = 1e-7) { + // expect(Math.abs(actual - expected)).toBeLessThanOrEqual(tolerance); + // } + + + expect(dangerSpace.begin.distance.In(Distance.Yard)).toBeCloseTo(393.6, 1e-1) + expect(dangerSpace.end.distance.In(Distance.Yard)).toBeCloseTo(579.0, 1e-1) + + // Second test + dangerSpace = shotResult.dangerSpace( + UNew.Yard(500), + UNew.Inch(10), + lookAngle + ); + + expect(dangerSpace.begin.distance.In(Distance.Yard)).toBeCloseTo(484.5, 1e-1) + expect(dangerSpace.end.distance.In(Distance.Yard)).toBeCloseTo(514.8, 1e-1) + + // assertAlmostEqual(dangerSpace.begin.distance.In(Distance.Yard), 484.5, 1); + // assertAlmostEqual(dangerSpace.end.distance.In(Distance.Yard), 514.8, 1); + }); +}); diff --git a/__tests__/v2/mbc.test.ts b/__tests__/v2/mbc.test.ts new file mode 100644 index 0000000..e749921 --- /dev/null +++ b/__tests__/v2/mbc.test.ts @@ -0,0 +1,107 @@ +import { expect, describe, test, beforeEach } from '@jest/globals'; +import Calculator, { UNew, DragModel, Table, Ammo, Weapon, Shot, DragModelMultiBC, BCPoint } from '../../src/v2'; + + +describe('TestMultiBC', () => { + let range = 1000; + let step = 100; + let dm = new DragModel({ bc: 0.22, dragTable: Table.G7 }); + let ammo = new Ammo({ dm: dm, mv: UNew.FPS(2600) }); + let weapon = new Weapon({ sightHeight: 4, twist: 12 }); + let calc = new Calculator() + let baseLineShot = new Shot({ weapon: weapon, ammo: ammo }) + let baselineTrajectory = calc.fire({ shot: baseLineShot, trajectoryRange: range, trajectoryStep: step }).trajectory + + beforeEach(() => { + + }) + + test("test_mbc1", () => { + let dmMulti = DragModelMultiBC({ + bcPoints: [ + new BCPoint({ BC: 0.22, V: UNew.FPS(2500) }), + new BCPoint({ BC: 0.22, V: UNew.FPS(1500) }), + new BCPoint({ BC: 0.22, Mach: 3 }) + ], + dragTable: Table.G7 + }); + let multiShot = new Shot({ weapon: weapon, ammo: new Ammo({ dm: dmMulti, mv: ammo.mv }) }) + let multiTrajectory = calc.fire({ shot: multiShot, trajectoryRange: range, trajectoryStep: step }).trajectory + + for (let i = 0; i < multiTrajectory.length; i++) { + expect(multiTrajectory[i].formatted()).toEqual(baselineTrajectory[i].formatted()); + } + }); + + test("test_mbc2", () => { + let dmMulti = DragModelMultiBC({ + bcPoints: [ + new BCPoint({ BC: 0.22, V: UNew.FPS(2700) }), + new BCPoint({ BC: .5, V: UNew.FPS(3500) }), + ], + dragTable: Table.G7 + }); + let multiShot = new Shot({ weapon: weapon, ammo: new Ammo({ dm: dmMulti, mv: ammo.mv }) }) + let multiTrajectory = calc.fire({ shot: multiShot, trajectoryRange: range, trajectoryStep: step }).trajectory + + for (let i = 0; i < multiTrajectory.length; i++) { + expect(multiTrajectory[i].formatted()).toEqual(baselineTrajectory[i].formatted()); + } + }); + + test("test_mbc3", () => { + let dmMulti = DragModelMultiBC({ + bcPoints: [ + new BCPoint({ BC: .5, V: baselineTrajectory[3].velocity }), + new BCPoint({ BC: .22, V: baselineTrajectory[2].velocity }), + ], + dragTable: Table.G7 + }); + let multiShot = new Shot({ weapon: weapon, ammo: new Ammo({ dm: dmMulti, mv: ammo.mv }) }) + let multiTrajectory = calc.fire({ shot: multiShot, trajectoryRange: range, trajectoryStep: step }).trajectory + + expect(multiTrajectory[1].velocity.rawValue).toBeCloseTo(baselineTrajectory[1].velocity.rawValue, 1) + expect(multiTrajectory[4].velocity.rawValue).toBeGreaterThan(baselineTrajectory[4].velocity.rawValue) + }); + + test("test_mbc", () => { + let dmMulti = DragModelMultiBC({ + bcPoints: [new BCPoint({ BC: 0.275, V: UNew.MPS(800) }), new BCPoint({ BC: 0.255, V: UNew.MPS(500) }), new BCPoint({ BC: 0.26, V: UNew.MPS(700) })], + dragTable: Table.G7, weight: 178, diameter: .308 + }); + + expect(dmMulti.dragTable[0].CD).toBeCloseTo(0.1259323091692403, 1e-8) + expect(dmMulti.dragTable[dmMulti.dragTable.length - 1].CD).toBeCloseTo(0.1577125859466895, 1e-8) + }); + + test("test_valid", () => { + let dmMulti = DragModelMultiBC({ + bcPoints: [ + new BCPoint({ BC: 0.417, V: UNew.MPS(745) }), + new BCPoint({ BC: 0.409, V: UNew.MPS(662) }), + new BCPoint({ BC: 0.4, V: UNew.MPS(580) }) + ], + dragTable: Table.G7, weight: 285, diameter: .338 + }); + + let cds = dmMulti.dragTable.map(p => p.CD); + let machs = dmMulti.dragTable.map(p => p.Mach); + + let reference = [ + [1, 0.3384895315], + [2, 0.2585639866], + [3, 0.2069547831], + [4, 0.1652052415], + [5, 0.1381406102], + ] + + reference.forEach(([mach, cd]) => { + const idx = machs.indexOf(mach); + + // Subtest equivalent using individual expect statements + expect(cds[idx]).toBeCloseTo(cd, 1e-3); // Precision of 3 decimal places + + }); + }); + +}); diff --git a/__tests__/v2/trajectory.test.ts b/__tests__/v2/trajectory.test.ts new file mode 100644 index 0000000..3db17ae --- /dev/null +++ b/__tests__/v2/trajectory.test.ts @@ -0,0 +1,179 @@ +import { describe, expect, test } from '@jest/globals'; +import Calculator, { Ammo, Atmo, DragModel, Table, Shot, TrajectoryCalc, UNew, Unit, Weapon, Wind, TrajFlag } from "../../src/v2"; + +describe("TrajectoryCalc", () => { + + function customAssertEqual(a, b, accuracy, name) { + test(name, () => { + expect(Math.abs(a - b)).toBeLessThan(accuracy) + }) + } + + function validateOne(data, distance, velocity, + mach, energy, path, hold, + windage, wind_adjustment, time, ogw, + adjustment_unit) { + + customAssertEqual(distance, data.distance.In(Unit.Yard), 0.001, "Distance") + customAssertEqual(velocity, data.velocity.In(Unit.FPS), 5, "Velocity") + customAssertEqual(mach, data.mach, 0.005, "Mach") + customAssertEqual(energy, data.energy.In(Unit.FootPound), 5, "Energy") + customAssertEqual(time, data.time, 0.06, "Time") + customAssertEqual(ogw, data.ogw.In(Unit.Pound), 1, "OGW") + + if (distance >= 800) { + customAssertEqual(path, data.height.In(Unit.Inch), 4, 'Height'); + } else if (distance >= 500) { + customAssertEqual(path, data.height.In(Unit.Inch), 1, 'Height'); + } else { + customAssertEqual(path, data.height.In(Unit.Inch), 0.5, 'Height'); + } + + if (distance > 1) { + customAssertEqual(hold, data.dropAdjustment.In(adjustment_unit), 0.5, 'Hold') + } + + if (distance >= 800) { + customAssertEqual(windage, data.windage.In(Unit.Inch), 1.5, "Windage") + } else if (distance >= 500) { + customAssertEqual(windage, data.windage.In(Unit.Inch), 1, "Windage") + } else { + customAssertEqual(windage, data.windage.In(Unit.Inch), 0.5, "Windage") + } + + if (distance > 1) { + customAssertEqual(wind_adjustment, + data.windageAdjustment.In(adjustment_unit), 0.5, "WindageAdjustment") + } + + test("Flag check", () => { + expect(data.flag & TrajFlag.RANGE).toBeTruthy() + } + ) + + } + + describe("test_zero1", () => { + const dm = new DragModel({ bc: 0.365, dragTable: Table.G1, weight: 69, diameter: 0.223, length: 0.9 }) + const ammo = new Ammo({ dm: dm, mv: 2600 }) + const weapon = new Weapon({ sightHeight: UNew.Inch(3.2) }) + const atmosphere = Atmo.icao({}) + const calc = new TrajectoryCalc(ammo) + + const zero_angle = calc.zeroAngle( + new Shot({ weapon: weapon, ammo: ammo, atmo: atmosphere }), + UNew.Yard(100) + ) + + test("check_zero", () => { + expect(zero_angle.In(Unit.Radian)).toBeCloseTo(0.001651, 1e-6) + }) + + }) + + describe("test_zero2", () => { + const dm = new DragModel({ bc: 0.223, dragTable: Table.G1, weight: 69, diameter: 0.223, length: 0.9 }) + const ammo = new Ammo({ dm: dm, mv: 2750 }) + const weapon = new Weapon({ twist: UNew.Inch(2) }) + + const atmosphere = Atmo.icao({}) + const calc = new TrajectoryCalc(ammo) + + const zero_angle = calc.zeroAngle( + new Shot({ ammo: ammo, weapon: weapon, atmo: atmosphere }), + UNew.Yard(100) + ) + + test("check_zero", () => { + expect(zero_angle.In(Unit.Radian)).toBeCloseTo(0.001228, 1e-6) + }) + + }) + + describe("test_path_g1", () => { + const dm = new DragModel({ bc: 0.223, dragTable: Table.G1, weight: 168, diameter: 0.308, length: 1.282 }) + const ammo = new Ammo({ dm: dm, mv: 2750 }) + const weapon = new Weapon({ sightHeight: UNew.Inch(2), zeroElevation: UNew.Radian(0.001228) }) + const atmo = Atmo.icao({}) + + const shot_info = new Shot({ + weapon: weapon, + ammo: ammo, + atmo: atmo, + winds: [ + new Wind({ velocity: UNew.MPH(5), directionFrom: UNew.OClock(10.5) }) + ] + }) + + const calc = new Calculator() + const data = calc.fire( + {shot: shot_info, + trajectoryRange: UNew.Yard(1000), + trajectoryStep: UNew.Yard(100),} + ).trajectory + + expect(data.length).toEqual(11) + + const test_data = [ + [data[0], 0, 2750, 2.463, 2820.6, -2, 0, 0, 0, 0, 880, Unit.MOA], + [data[1], 100, 2351.2, 2.106, 2061, 0, 0, -0.6, -0.6, 0.118, 550, Unit.MOA], + [data[5], 500, 1169.1, 1.047, 509.8, -87.9, -16.8, -19.5, -3.7, 0.857, 67, Unit.MOA], + [data[10], 1000, 776.4, 0.695, 224.9, -823.9, -78.7, -87.5, -8.4, 2.495, 20, Unit.MOA] + ] + + test_data.forEach(item => { + describe(`validateOne ${test_data.indexOf(item)}`, () => { + validateOne( + ...item + ) + } + ) + }) + + }) + + describe("test_path_g7", () => { + const dm = new DragModel({ bc: 0.223, dragTable: Table.G7, weight: 168, diameter: 0.308, length: 1.282 }) + const ammo = new Ammo({ dm: dm, mv: UNew.FPS(2750) }) + const weapon = new Weapon({ sightHeight: UNew.Inch(2), twist: UNew.Inch(12), zeroElevation: UNew.MOA(4.221) }) + const atmo = Atmo.icao({}) + + const shot_info = new Shot({ + weapon: weapon, + ammo: ammo, + atmo: atmo, + winds: [ + new Wind({ velocity: UNew.MPH(5), directionFrom: UNew.Degree(-45) }) + ] + }) + + const calc = new Calculator() + const data = calc.fire( + {shot: shot_info, + trajectoryRange: UNew.Yard(1000), + trajectoryStep: UNew.Yard(100),} + ).trajectory + + expect(data.length).toEqual(11) + + const test_data = [ + [data[0], 0, 2750, 2.46, 2821, -2.0, 0.0, 0.0, 0.0, 0.0, 880, Unit.MIL], + [data[1], 100, 2545, 2.28, 2416, 0.0, 0.0, -0.2, -0.06, 0.113, 698, Unit.MIL], + [data[5], 500, 1814, 1.62, 1227, -56.2, -3.2, -6.3, -0.36, 0.672, 252, Unit.MIL], + [data[10], 1000, 1086, 0.97, 440, -399.9, -11.3, -31.6, -0.9, 1.748, 54, Unit.MIL] + ] + + test_data.forEach(item => { + describe(`validateOne ${test_data.indexOf(item)}`, () => { + validateOne( + ...item + ) + } + ) + + }) + + }) + + + }); \ No newline at end of file diff --git a/__tests__/v2/unit.test.ts b/__tests__/v2/unit.test.ts new file mode 100644 index 0000000..1ed1556 --- /dev/null +++ b/__tests__/v2/unit.test.ts @@ -0,0 +1,130 @@ +import {describe, expect, test} from '@jest/globals'; +import {Measure, UNew, Unit, UnitProps, unitTypeCoerce} from '../../src/v2'; + +describe("Unit back'n'forth", () => { + + + function backAndForth(value: number, units: Unit): void { + const u = UNew[units](value); + const v = u.In(units); + + expect(v).toBeCloseTo(value, 7) + + test(`Read back failed for ${UnitProps[units].name}`, () => { + expect(v).toBeCloseTo(value, 7) + }) + + test(`Conversion ${UnitProps[units].name}`, () => { + expect(u.to(units).rawValue).toBeCloseTo(u.rawValue, 7) + }) + } + + function backAndForthAll(unitList: Unit[]): void { + unitList.forEach((unit) => { + backAndForth(3, unit); + }); + } + + describe('Angular', () => { + backAndForthAll([ + Unit.Degree, + Unit.MOA, + Unit.MRad, + Unit.MIL, + Unit.Radian, + Unit.Thousand, + ]); + }); + + describe('Distance', () => { + backAndForthAll([ + Unit.Centimeter, + Unit.Foot, + Unit.Inch, + Unit.Kilometer, + Unit.Line, + Unit.Meter, + Unit.Millimeter, + Unit.Mile, + Unit.NauticalMile, + Unit.Yard, + ]); + }); + + describe('Energy', () => { + backAndForthAll([ + Unit.FootPound, + Unit.Joule, + ]); + }); + + describe('Pressure', () => { + backAndForthAll([ + Unit.Bar, + Unit.hPa, + Unit.MmHg, + Unit.InHg, + ]); + }); + + describe('Temperature', () => { + backAndForthAll([ + Unit.Fahrenheit, + Unit.Kelvin, + Unit.Celsius, + Unit.Rankin, + ]); + }); + + describe('Velocity', () => { + backAndForthAll([ + Unit.FPS, + Unit.KMH, + Unit.KT, + Unit.MPH, + Unit.MPS, + ]); + }); + + describe('Weight', () => { + backAndForthAll([ + Unit.Grain, + Unit.Gram, + Unit.Kilogram, + Unit.Newton, + Unit.Ounce, + Unit.Pound + ]); + }); +}); + + +describe("Unit coercion", () => { + + test("As number", () => { + const unit = unitTypeCoerce(10, Measure.Distance, Unit.Yard) + expect(unit.In(Unit.Yard)).toBeCloseTo(10, 7) + }); + + test("As AbstractUnit", () => { + const unit = unitTypeCoerce(UNew.Yard(10), Measure.Distance, Unit.Yard) + expect(unit.In(Unit.Yard)).toBeCloseTo(10, 7) + }); + + test("As invalid value", () => { + // @ts-ignore + expect(() => unitTypeCoerce("invalid", Measure.Distance, Unit.Yard)) + .toThrowError(`Instance must be a type of ${ + Measure.Distance.name + } or 'number'`); + }); + + test("As undefined", () => { + //@ts-ignore + expect(() => unitTypeCoerce(undefined, Measure.Distance, Unit.Yard)) + .toThrowError(`Instance must be a type of ${ + Measure.Distance.name + } or 'number'`); + }); + +}); diff --git a/package-lock.json b/package-lock.json index 6a18b68..3e7a886 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,19 @@ { "name": "js-ballistics", - "version": "1.1.2", + "version": "2.0.0-beta.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "js-ballistics", - "version": "1.1.2", + "version": "2.0.0-beta.0", "license": "ISC", "devDependencies": { "@babel/core": "^7.23.6", "@babel/preset-env": "^7.23.6", "@babel/preset-typescript": "^7.23.3", "@jest/globals": "^29.7.0", + "@types/jest": "^29.5.12", "babel-jest": "^29.7.0", "jest": "^29.7.0", "ts-jest": "^29.1.1" @@ -2362,6 +2363,16 @@ "@types/istanbul-lib-report": "*" } }, + "node_modules/@types/jest": { + "version": "29.5.12", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.12.tgz", + "integrity": "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==", + "dev": true, + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, "node_modules/@types/node": { "version": "20.10.4", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.4.tgz", diff --git a/package.json b/package.json index 302aa5e..67b72ae 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "js-ballistics", - "version": "1.1.2", + "version": "2.0.0-beta.0", "description": "ISC library for small arms ballistic calculations (JavaScript ES6+)", "main": "dist/index.js", "scripts": { @@ -34,6 +34,7 @@ "@babel/preset-env": "^7.23.6", "@babel/preset-typescript": "^7.23.3", "@jest/globals": "^29.7.0", + "@types/jest": "^29.5.12", "babel-jest": "^29.7.0", "jest": "^29.7.0", "ts-jest": "^29.1.1" diff --git a/src/v2/conditions.ts b/src/v2/conditions.ts new file mode 100644 index 0000000..d98ac35 --- /dev/null +++ b/src/v2/conditions.ts @@ -0,0 +1,437 @@ +// Classes to define zeroing or current environment conditions + +import { Ammo, Weapon } from './munition'; +import { + Unit, unitTypeCoerce, UNew, + Distance, Pressure, Temperature, Velocity, Angular, + preferredUnits +} from './unit'; + +// Constants for standard atmospheric conditions +export const cStandardHumidity: number = 0.0 // Relative Humidity +export const cPressureExponent: number = 5.255876 // =g*M/R*L +export const cA0: number = 1.24871 +export const cA1: number = 0.0988438; +export const cA2: number = 0.00152907; +export const cA3: number = -3.07031e-06; +export const cA4: number = 4.21329e-07; +export const cA5: number = 3.342e-04; + +// ISA, metric prefer_units: (https://www.engineeringtoolbox.com/international-standard-atmosphere-d_985.html) +export const cDegreesCtoK: number = 273.15; // °K = °C + 273.15 +export const cStandardTemperatureC: number = 15.0; // °C +export const cLapseRateMetric: number = -6.5e-03; // Lapse Rate, °C/m +export const cStandardPressureMetric: number = 1013.25; // hPa +export const cSpeedOfSoundMetric: number = 331.3; // Mach1 in m/s = cSpeedOfSound * sqrt(°K) +export const cStandardDensityMetric: number = 1.2250; // kg/m^3 +export const cDensityImperialToMetric: number = 16.0185; // lb/ft^3 to kg/m^3 + +// ICAO standard atmosphere: +export const cDegreesFtoR: number = 459.67 // °R = °F + 459.67 +export const cStandardTemperatureF: number = 59.0 // °F +export const cLapseRateImperial: number = -3.56616e-03 // Lapse rate, °F/ft +export const cStandardPressure: number = 29.92 // InHg +export const cSpeedOfSoundImperial: number = 49.0223 // Mach1 in fps = cSpeedOfSound * sqrt(°R) +export const cStandardDensity: number = 0.076474 // lb/ft^3 + + + +class Atmo { + + readonly altitude: Distance + readonly pressure: Pressure + readonly temperature: Temperature + readonly humidity: number + + readonly densityRatio: number + readonly mach: Velocity + + protected _mach1: number + protected _a0: number + protected _t0: number + protected _p0: number + protected _ta: number + + /** + * Represents atmospheric conditions and performs density calculations. + * + * @param {Object} [options] - The options for initializing the atmospheric conditions. + * @param {number | Distance | null} [options.altitude=null] - Altitude above sea level, or a distance object. + * @param {number | Pressure | null} [options.pressure=null] - Atmospheric pressure, or a pressure object. + * @param {number | Temperature | null} [options.temperature=null] - Temperature in Fahrenheit, or a temperature object. + * @param {number} [options.humidity=0.0] - Relative humidity as a decimal (default: 0.0, where 1.0 is 100%). + */ + constructor({ + altitude = null, + pressure = null, + temperature = null, + humidity = 0.0 + }: { + altitude?: (number | Distance | null); + pressure?: (number | Pressure | null); + temperature?: (number | Temperature | null); + humidity?: number + } + ) { + // Ensure humidity is within the valid range [0, 1] + this.humidity = humidity || 0.0; + if (humidity > 1.0) { + this.humidity = humidity / 100.0; + } + if (!(0.0 <= this.humidity && this.humidity <= 1.0)) { + this.humidity = 0.0; + } + + // Coerce input values to appropriate units + this.altitude = unitTypeCoerce(altitude ?? 0, Distance, preferredUnits.distance); + this.pressure = unitTypeCoerce(pressure ?? Atmo.standardPressure(this.altitude), Pressure, preferredUnits.pressure); + this.temperature = unitTypeCoerce(temperature ?? Atmo.standardTemperature(this.altitude), Temperature, preferredUnits.temperature); + + // Constants and initializations + this._t0 = this.temperature.In(Unit.Fahrenheit); + this._p0 = this.pressure.In(Unit.InHg); + this._a0 = this.altitude.In(Unit.Foot); + this._ta = this._a0 * cLapseRateImperial + cStandardTemperatureF; + this.densityRatio = this.calculateDensity({ + t: this._t0, + p: this._p0 + }) / cStandardDensity + this._mach1 = Atmo.machF(this._t0); + this.mach = UNew.FPS(this._mach1) + } + + /** + * Calculates the ICAO standard temperature at a given altitude. + * + * The standard temperature decreases with altitude at a fixed rate (lapse rate). + * + * @param {Distance} altitude - The altitude above sea level. + * @returns {Temperature} - The standard temperature at the given altitude in Fahrenheit. + */ + static standardTemperature(altitude: Distance): Temperature { + return UNew.Fahrenheit(cStandardTemperatureF + (altitude.In(Distance.Foot)) * cLapseRateImperial) + } + + /** + * Calculates the ICAO standard pressure at a given altitude. + * + * The pressure decreases with altitude according to a fixed mathematical model. + * + * @param {Distance} altitude - The altitude above sea level. + * @returns {Pressure} - The standard atmospheric pressure at the given altitude in inches of mercury (InHg). + */ + static standardPressure(altitude: Distance): Pressure { + // ICAO standard pressure for altitude + return UNew.InHg(0.02953 * Math.pow(3.73145 - 2.56555e-05 * altitude.In(Distance.Foot), cPressureExponent)) + } + + /** + * Creates a standard ICAO atmosphere based on the given altitude and temperature. + * + * If the temperature is not provided, the standard ICAO temperature for the given altitude is used. + * + * @param {Object} options - Configuration options for the atmosphere. + * @param {number | Distance | null} [options.altitude=null] - The altitude above sea level. If not provided, defaults to null. + * @param {number | Temperature | null} [options.temperature=null] - The temperature in Fahrenheit. If not provided, defaults to the standard temperature for the given altitude. + * @returns {Atmo} - An instance of the `Atmo` class representing the atmospheric conditions. + */ + static standard({ + altitude = null, + temperature = null + }: { + altitude?: (number | Distance | null); + temperature?: (number | Temperature | null) + }): Atmo { + return Atmo.icao({ altitude: altitude, temperature: temperature }) + } + + /** + * Creates an ICAO standard atmosphere based on the given altitude and temperature. + * + * If the temperature is not specified, the standard ICAO temperature for the altitude is used. + * The method also calculates the standard pressure for the given altitude. + * + * @param {Object} options - Configuration options for the atmosphere. + * @param {number | Distance | null} [options.altitude=null] - The altitude above sea level. If not provided, defaults to 0. + * @param {number | Temperature | null} [options.temperature=null] - The temperature in Fahrenheit. If not provided, the standard temperature for the altitude is used. + * @returns {Atmo} - An instance of the `Atmo` class representing the ICAO atmosphere with the given conditions. + */ + static icao({ + altitude = null, + temperature = null + }: { + altitude?: (number | Distance | null); + temperature?: (number | Temperature | null) + }): Atmo { + const _altitude: Distance = unitTypeCoerce(altitude ?? 0, Distance, preferredUnits.distance) + const _temperature: Temperature = unitTypeCoerce(temperature ?? Atmo.standardTemperature(_altitude), Temperature, preferredUnits.temperature) + const _pressure: Pressure = Atmo.standardPressure(_altitude) + return new Atmo({ + altitude: _altitude.In(preferredUnits.distance), + pressure: _pressure.In(preferredUnits.pressure), + temperature: _temperature.In(preferredUnits.temperature), + humidity: cStandardHumidity + } + ) + } + + /** + * Calculates the speed of sound (Mach 1) in feet per second (fps) at a given temperature in Fahrenheit. + * + * @param {number} fahrenheit - The temperature in Fahrenheit. + * @returns {number} - The speed of sound (Mach 1) in fps at the given temperature. + */ + static machF(fahrenheit: number): number { + return Math.sqrt(fahrenheit + cDegreesFtoR) * cSpeedOfSoundImperial + } + + /** + * Calculates the speed of sound (Mach 1) in meters per second (m/s) at a given temperature in Celsius. + * + * @param {number} celsius - The temperature in Celsius. + * @returns {number} - The speed of sound (Mach 1) in m/s at the given temperature. + */ + static machC(celsius: number): number { + return Math.sqrt(1 + celsius / cDegreesCtoK) * cSpeedOfSoundMetric + } + + /** + * Calculates the density of air based on temperature, pressure, and humidity. + * + * The calculation uses the formula for humid air, considering the partial pressures of dry air and water vapor. + * + * @param {Object} options - Parameters for air density calculation. + * @param {Temperature} options.temperature - The air temperature. + * @param {Pressure} options.pressure - The atmospheric pressure. + * @param {number} options.humidity - The relative humidity as a decimal (where 1.0 is 100%). + * @returns {number} - The air density in Imperial units (lb/ft³). + * + * @see https://en.wikipedia.org/wiki/Density_of_air#Humid_air + */ + static airDensity({ + temperature, + pressure: pressure, + humidity + }: { + temperature: Temperature, + pressure: Pressure, + humidity: number + }): number { + // Density in Imperial units (lb/ft^3) + const tC = temperature.In(Temperature.Celsius) + const pM = pressure.In(Pressure.hPa) * 100 + const psat = 6.1078 * Math.pow(10, 17.27 * tC / (tC + 237.3)) + const pv = humidity * psat // Pressure of water vapor in Pascals + const pd = pM - pv // Partial pressure of dry air in Pascals + // Density in metric units kg/m^3 + const density = (pd * 0.0289652 + pv * 0.018016) / (8.31446 * (tC + cDegreesCtoK)) + return density / cDensityImperialToMetric + } + + /** + * Gets the air density in metric units (kg/m³). + * + * The density is calculated based on the `densityRatio` and the standard air density at sea level in metric units. + * + * @returns {number} - The air density in kilograms per cubic meter (kg/m³). + */ + get densityMetric(): number { + const cStandardDensityMetric = 1.225; // Standard air density at sea level (kg/m^3) + return this.densityRatio * cStandardDensityMetric; + } + + /** + * Gets the air density in imperial units (lb/ft³). + * + * The density is calculated based on the `densityRatio` and the standard air density at sea level in imperial units. + * + * @returns {number} - The air density in pounds per cubic foot (lb/ft³). + */ + get densityImperial(): number { + const cStandardDensity = 0.0765; // Standard air density at sea level (lb/ft^3) + return this.densityRatio * cStandardDensity; + } + + /** + * Calculates the interpolated temperature at a given altitude above sea level (ASL). + * + * This method uses the standard lapse rate to adjust the temperature based on the altitude difference from the reference altitude. + * + * @param {number} altitude - The altitude above sea level (in feet). + * @returns {number} - The temperature at the given altitude in degrees Fahrenheit (°F). + */ + temperatureAtAltitude(altitude: number): number { + return (altitude - this._a0) * cLapseRateImperial + this._t0 + } + + /** + * Calculates air density based on the given temperature and pressure, adjusting for the specified atmosphere conditions. + * + * The calculation accounts for humidity's effect on air density and uses temperature in degrees Fahrenheit and pressure in inches of mercury. + * + * @param {Object} options - The parameters for the density calculation. + * @param {number} options.t - The temperature in degrees Fahrenheit (°F). + * @param {number} options.p - The atmospheric pressure in inches of mercury (inHg). + * @returns {number} - The air density in pounds per cubic foot (lb/ft³). + */ + calculateDensity({ t, p }: { t: number, p: number }): number { + let hc + if (t > 0) { + const et0 = cA0 + t * (cA1 + t * (cA2 + t * (cA3 + t * cA4))) + const et = cA5 * this.humidity * et0 + hc = (p - 0.3783 * et) / cStandardPressure + } else { + hc = 1.0 + } + return cStandardDensity * ((cStandardTemperatureF + cDegreesFtoR) / (t + cDegreesFtoR)) * hc + } + + /** + * Gets the air density factor and Mach number for a given altitude. + * + * If the altitude is within 30 feet of the current altitude, the method returns the current density ratio and Mach number. + * For other altitudes, it uses an exponential approximation for the air density ratio and calculates the Mach number based on the interpolated temperature at the given altitude. + * + * @param {number} altitude - The altitude above sea level in feet. + * @returns {[number, number]} - A tuple containing: + * - The air density ratio relative to the standard density. + * - The Mach number at the given altitude. + */ + getDensityFactorAndMachForAltitude(altitude: number): [number, number] { + if (Math.abs(this._a0 - altitude) < 30) { + return [this.densityRatio, this._mach1]; + } else { + // Exponential approximation for air density ratio + const densityRatio = Math.exp(-altitude / 34112.0); + const temperature = this.temperatureAtAltitude(altitude); + const mach = Atmo.machF(temperature); + return [densityRatio, mach]; + } + } +} + + +class Wind { + + readonly velocity: Velocity + readonly directionFrom: Angular + readonly untilDistance: Distance + public static MAX_DISTANCE_FEET: number = 1e8 + + /** + * Stores wind data at the desired distance. + * + * @param {Object} [options] - The options for initializing wind data. + * @param {number | Velocity | null} [options.velocity=null] - Wind velocity. Can be a number, a `Velocity` object, or `null`. + * @param {number | Angular | null} [options.directionFrom=null] - Wind direction in relation to the shooter. Can be a number, an `Angular` object, or `null`. + * @param {number | Distance | null} [options.untilDistance=null] - Distance up to which the wind data is applicable. Can be a number, a `Distance` object, or `null`. + * @param {number} [options.maxDistanceFeet=1e8] - Maximum distance in feet up to which the wind data is applicable. Defaults to `1e8`. + */ + constructor({ + velocity = null, + directionFrom = null, + untilDistance = null, + maxDistanceFeet = 1e8 + }: { + velocity?: (number | Velocity | null); + directionFrom?: (number | Angular | null); + untilDistance?: (number | Distance | null); + maxDistanceFeet?: (number | null) + }) { + // Coerce input values to appropriate units + Wind.MAX_DISTANCE_FEET = maxDistanceFeet ?? 1e8 + this.velocity = unitTypeCoerce(velocity ?? 0, Velocity, preferredUnits.velocity); + this.directionFrom = unitTypeCoerce(directionFrom ?? 0, Angular, preferredUnits.angular); + this.untilDistance = unitTypeCoerce(untilDistance ?? UNew.Foot(Wind.MAX_DISTANCE_FEET), Distance, preferredUnits.distance); + } +} + +/** + * Represents the parameters required for calculating a shot's trajectory. + */ +class Shot { + + weapon: Weapon; + ammo: Ammo; + lookAngle: Angular; + relativeAngle: Angular; + cantAngle: Angular; + atmo: Atmo; + winds: Wind[]; + + /** + * Creates an instance of the Shot class. + * + * @param {Object} options - The parameters for initializing the shot data. + * @param {Weapon} options.weapon - The weapon used for the shot. + * @param {Ammo} options.ammo - The ammunition used for the shot. + * @param {number | Angular | null} [options.lookAngle=null] - The angle of the shot relative to the horizontal plane. Can be a number, an `Angular` object, or `null`. + * @param {number | Angular | null} [options.relativeAngle=null] - The angle between the shot's trajectory and the intended target line. Can be a number, an `Angular` object, or `null`. + * @param {number | Angular | null} [options.cantAngle=null] - The angle of cant or tilt of the weapon. Can be a number, an `Angular` object, or `null`. + * @param {Atmo | null} [options.atmo=null] - The atmospheric conditions affecting the shot. Can be an `Atmo` object or `null`. + * @param {Wind[] | null} [options.winds=null] - List of wind conditions affecting the shot. Can be an array of `Wind` objects or `null`. + */ + constructor({ + weapon, + ammo, + lookAngle = null, + relativeAngle = null, + cantAngle = null, + atmo = null, + winds = null + }: { + weapon: Weapon; + ammo: Ammo; + lookAngle?: number | Angular | null; + relativeAngle?: number | Angular | null; + cantAngle?: number | Angular | null; + atmo?: Atmo | null; + winds?: Wind[] | null; + }) { + this.lookAngle = unitTypeCoerce(lookAngle ?? 0, Angular, preferredUnits.angular); + this.relativeAngle = unitTypeCoerce(relativeAngle ?? 0, Angular, preferredUnits.angular); + this.cantAngle = unitTypeCoerce(cantAngle ?? 0, Angular, preferredUnits.angular); + this.weapon = weapon + this.ammo = ammo; + this.atmo = atmo ?? Atmo.icao({}) + this.winds = (winds ?? [new Wind({})]) + .slice() // Create a copy of the array + .sort((a, b) => a.untilDistance.rawValue - b.untilDistance.rawValue); + } + + /** + * Gets the barrel elevation in the vertical plane from the horizontal. + * + * The elevation is calculated by adding the look angle to the vertical component of + * the barrel's elevation based on the cant angle and relative angle. The result is + * converted to radians. + * + * @returns {Angular} The barrel elevation in radians. + */ + get barrelElevation(): Angular { + return UNew.Radian( + this.lookAngle.In(Angular.Radian) + Math.cos(this.cantAngle.In(Angular.Radian)) * ( + this.weapon.zeroElevation.In(Angular.Radian) + this.relativeAngle.In(Angular.Radian) + ) + ) + } + + /** + * Gets the horizontal angle of the barrel relative to the sight line. + * + * The azimuth angle is calculated based on the cant angle and the relative angle of the + * weapon. The result is converted to radians. + * + * @returns {Angular} The barrel azimuth in radians. + */ + get barrelAzimuth(): Angular { + return UNew.Radian( + Math.sin(this.cantAngle.In(Angular.Radian)) * ( + this.weapon.zeroElevation.In(Angular.Radian) + this.relativeAngle.In(Angular.Radian) + ) + ) + } +} + + +export { Atmo, Wind, Shot }; diff --git a/src/v2/drag_model.ts b/src/v2/drag_model.ts new file mode 100644 index 0000000..ee2064d --- /dev/null +++ b/src/v2/drag_model.ts @@ -0,0 +1,301 @@ +// Import necessary modules and classes +import { Distance, unitTypeCoerce, Weight, Velocity, preferredUnits, } from './unit'; +// @ts-ignore +import Table from './drag_tables.js' + + +export const cSpeedOfSoundMetric: number = 340.0 + +/** + * Represents a data point for drag calculation. + */ +class DragDataPoint { + /** + * @param {number} Mach - Mach number at the data point. + * @param {number} CD - Drag coefficient at the data point. + */ + constructor(public Mach: number, public CD: number) { } +} + +/** + * Type alias for drag table data. + * Can be an array of objects with Mach and CD properties or DragDataPoint instances. + */ +type DragTableDataType = Array<{ Mach: number, CD: number } | DragDataPoint>; + +/** + * Type alias for an array of DragDataPoint instances. + */ +type DragTable = DragDataPoint[] + +/** + * Represents a ballistic coefficient point. + */ +class BCPoint { + readonly BC: number + readonly Mach: (number) + readonly V: (Velocity | null) + + /** + * Creates an instance of BCPoint. + * @param {Object} options - The parameters for initializing the ballistic coefficient point. + * @param {number} options.BC - The ballistic coefficient. Must be positive. + * @param {number} [options.Mach=null] - Mach number. Optional if velocity is provided. + * @param {number | Velocity | null} [options.V=null] - Velocity. Optional if Mach number is provided. + * @throws {Error} If BC is less than or equal to zero, or if both Mach and V are specified, or if neither Mach nor V is specified. + */ + constructor({ + BC, + Mach = null, + V = null + }: { + BC: number, + Mach?: (number | null), + V?: (number | Velocity | null) + } + ) { + if (BC <= 0) { + throw new Error("Ballistic coefficient must be positive") + } + + if (Mach && V) { + throw new Error("You cannot specify both 'Mach' and 'V' at the same time") + } + + if (!Mach && !V) { + throw new Error("One of 'Mach' and 'V' must be specified") + } + + this.BC = BC + this.V = V ? unitTypeCoerce(V, Velocity, preferredUnits.velocity) : null + this.Mach = this.V ? this.V.In(Velocity.MPS) / cSpeedOfSoundMetric : (Mach ? Mach : 0) + } +} + + +// Define the DragModel class +class DragModel { + /** + * Constructor for DragModel class. + * @param {number} bc - Coefficient value for drag. + * @param {DragTable} dragTable - Custom drag table. + * @param {number|Weight} weight - Weight value or Weight instance. + * @param {number|Distance} diameter - Diameter value or Distance instance. + * @param {number|Distance} length - Diameter value or Distance instance. + */ + + readonly bc: number; + readonly dragTable: DragTable; + readonly weight: Weight; + readonly diameter: Distance; + readonly length: Distance; + + protected sectionalDensity: number; + protected formFactor: number; + + /** + * Creates an instance of DragModel. + * @param {Object} options - The options for initializing the drag model. + * @param {number} options.bc - Coefficient value for drag. + * @param {DragTable} options.dragTable - Custom drag table. + * @param {number | Weight} [options.weight=0] - Weight value or Weight instance (default: 0). + * @param {number | Distance} [options.diameter=0] - Diameter value or Distance instance (default: 0). + * @param {number | Distance} [options.length=0] - Length value or Distance instance (default: 0). + */ + constructor({ + bc, + dragTable, + weight = 0, + diameter = 0, + length = 0 + }: { + bc: number, + dragTable: DragTableDataType, + weight?: (number | Weight), + diameter?: (number | Distance), + length?: (number | Distance) + } + ) { + // Get the length of the dragTable + const tableLen = dragTable.length; + + // Check if the table length is not greater than 0 + if (tableLen <= 0) { + throw new Error('Received empty drag table'); + } else if (bc <= 0) { + // Check if the drag coefficient is not greater than zero + throw new Error('Ballistic coefficient must be positive'); + } + + this.dragTable = makeDataPoints(dragTable) + + this.bc = bc + this.weight = unitTypeCoerce(weight ?? 0, Weight, preferredUnits.weight); + this.diameter = unitTypeCoerce(diameter ?? 0, Distance, preferredUnits.diameter); + this.length = unitTypeCoerce(length ?? 0, Distance, preferredUnits.length); + // Calculate and set the sectional density and form factor + if (weight && diameter) { + this.sectionalDensity = this._getSectionalDensity(); + this.formFactor = this._getFormFactor(this.bc); + } + } + + /** + * Calculate and return the form factor. + * @param {number} bc - Drag coefficient value. + * @returns {number} - Calculated form factor. + * @private + */ + _getFormFactor(bc: number): number { + // Divide sectional density by drag coefficient + return this.sectionalDensity / bc; + } + + /** + * Calculate and return the sectional density. + * @returns {number} - Calculated sectional density. + * @private + */ + _getSectionalDensity(): number { + // Get weight in grains and diameter in inches + const w = this.weight.In(Weight.Grain); + const d = this.diameter.In(Distance.Inch); + // Call the sectionalDensity function to calculate and return the result + return sectionalDensity(w, d); + } +} + +/** + * Converts a drag table into an array of `DragDataPoint` objects. + * @param {DragTableDataType} dragTable - The input drag table data, which can be a mix of `DragDataPoint` instances and objects with `Mach` and `CD` properties. + * @returns {DragDataPoint[]} - An array of `DragDataPoint` objects. + * @throws {TypeError} - If any item in the drag table is not a `DragDataPoint` or an object with `Mach` and `CD` properties. + */ +function makeDataPoints(dragTable: DragTableDataType): DragDataPoint[] { + return dragTable.map(point => { + if (point instanceof DragDataPoint) { + return point; // If already a DragDataPoint, return it + } else if ('Mach' in point && 'CD' in point) { + // If it's a dictionary with 'Mach' and 'CD', create a new DragDataPoint + return new DragDataPoint(point.Mach, point.CD); + } else { + throw new TypeError("All items in dragTable must be of type DragDataPoint or an object with 'Mach' and 'CD' keys."); + } + }); +} + +/** + * Calculates and returns the sectional density. + * @param {number} weight - The weight value (in grains). + * @param {number} diameter - The diameter value (in inches). + * @returns {number} - The calculated sectional density (in lb/in²). + */ +function sectionalDensity(weight: number, diameter: number): number { + // Divide weight by the square of diameter and then by 7000 + return weight / Math.pow(diameter, 2) / 7000; +} + +/** + * Creates a `DragModel` instance with multiple ballistic coefficient (BC) points. + * @param {Object} options - The options for initializing the `DragModel`. + * @param {BCPoint[]} options.bcPoints - An array of `BCPoint` objects representing the ballistic coefficients. + * @param {DragTableDataType} options.dragTable - The drag table data, which can be a mix of `DragDataPoint` instances and objects with `Mach` and `CD` properties. + * @param {number | Weight} [options.weight=0] - The weight value or a `Weight` instance. Defaults to 0. + * @param {number | Distance} [options.diameter=0] - The diameter value or a `Distance` instance. Defaults to 0. + * @param {number | Distance} [options.length=0] - The length value or a `Distance` instance. Defaults to 0. + * @returns {DragModel} - An instance of `DragModel` initialized with the provided options. + */ +function DragModelMultiBC({ + bcPoints, + dragTable, + weight = 0, + diameter = 0, + length = 0 +}: { + bcPoints: BCPoint[]; + dragTable: DragTableDataType; + weight?: (number | Weight); + diameter?: (number | Distance); + length?: (number | Distance) +}): DragModel { + + let bc: number + const _weight = unitTypeCoerce(weight ?? 0, Weight, preferredUnits.weight); + const _diameter = unitTypeCoerce(diameter ?? 0, Distance, preferredUnits.diameter); + if (_weight.rawValue > 0 && _diameter.rawValue > 0) { + bc = sectionalDensity(_weight.In(Weight.Grain), _diameter.In(Distance.Inch)) + } else { + bc = 1.0 + } + + const _dragTable = makeDataPoints(dragTable) + bcPoints.sort((a, b) => a.Mach - b.Mach); + const bcInterp = linearInterpolation( + _dragTable.map((point) => point.Mach), + bcPoints.map((point) => point.Mach), + bcPoints.map((point) => point.BC / bc) + ) + + _dragTable.forEach((item, index) => { + item.CD = item.CD / bcInterp[index]; + }); + + return new DragModel({ bc: bc, dragTable: _dragTable, weight: _weight, diameter: _diameter, length: length }); +} + +/** + * Performs linear interpolation based on the provided x-values, x-coordinates, and y-values. + * @param {number[]} x - The x-values at which interpolation is to be performed. + * @param {number[]} xp - The x-coordinates of the data points used for interpolation. + * @param {number[]} yp - The y-values of the data points used for interpolation. + * @returns {number[]} - An array of interpolated y-values corresponding to the x-values. + * @throws {Error} - Throws an error if the lengths of `xp` and `yp` do not match, or if `x` is empty. + */ +function linearInterpolation( + x: number[], + xp: number[], + yp: number[] +): number[] { + if (xp.length !== yp.length) { + throw new Error("xp and yp lists must have the same length"); + } + + const y: number[] = []; + + for (const xi of x) { + if (xi <= xp[0]) { + y.push(yp[0]); + } else if (xi >= xp[xp.length - 1]) { + y.push(yp[yp.length - 1]); + } else { + let left = 0; + let right = xp.length - 1; + + while (left < right) { + const mid = Math.floor((left + right) / 2); + + if (xp[mid] <= xi && xi < xp[mid + 1]) { + const slope = (yp[mid + 1] - yp[mid]) / (xp[mid + 1] - xp[mid]); + y.push(yp[mid] + slope * (xi - xp[mid])); + break; + } + + if (xi < xp[mid]) { + right = mid; + } else { + left = mid + 1; + } + } + + if (left === right) { + y.push(yp[left]); + } + } + } + + return y; +} + +export type { Table, DragTable, DragTableDataType }; +export { DragDataPoint, BCPoint, DragModelMultiBC } +export default DragModel; diff --git a/src/v2/drag_tables.js b/src/v2/drag_tables.js new file mode 100644 index 0000000..6884c44 --- /dev/null +++ b/src/v2/drag_tables.js @@ -0,0 +1,2592 @@ +const Table = { + G1: [ + { + Mach: 0.00, + CD: 0.2629 + }, + { + Mach: 0.05, + CD: 0.2558 + }, + { + Mach: 0.10, + CD: 0.2487 + }, + { + Mach: 0.15, + CD: 0.2413 + }, + { + Mach: 0.20, + CD: 0.2344 + }, + { + Mach: 0.25, + CD: 0.2278 + }, + { + Mach: 0.30, + CD: 0.2214 + }, + { + Mach: 0.35, + CD: 0.2155 + }, + { + Mach: 0.40, + CD: 0.2104 + }, + { + Mach: 0.45, + CD: 0.2061 + }, + { + Mach: 0.50, + CD: 0.2032 + }, + { + Mach: 0.55, + CD: 0.2020 + }, + { + Mach: 0.60, + CD: 0.2034 + }, + { + Mach: 0.70, + CD: 0.2165 + }, + { + Mach: 0.725, + CD: 0.2230 + }, + { + Mach: 0.75, + CD: 0.2313 + }, + { + Mach: 0.775, + CD: 0.2417 + }, + { + Mach: 0.80, + CD: 0.2546 + }, + { + Mach: 0.825, + CD: 0.2706 + }, + { + Mach: 0.85, + CD: 0.2901 + }, + { + Mach: 0.875, + CD: 0.3136 + }, + { + Mach: 0.90, + CD: 0.3415 + }, + { + Mach: 0.925, + CD: 0.3734 + }, + { + Mach: 0.95, + CD: 0.4084 + }, + { + Mach: 0.975, + CD: 0.4448 + }, + { + Mach: 1.0, + CD: 0.4805 + }, + { + Mach: 1.025, + CD: 0.5136 + }, + { + Mach: 1.05, + CD: 0.5427 + }, + { + Mach: 1.075, + CD: 0.5677 + }, + { + Mach: 1.10, + CD: 0.5883 + }, + { + Mach: 1.125, + CD: 0.6053 + }, + { + Mach: 1.15, + CD: 0.6191 + }, + { + Mach: 1.20, + CD: 0.6393 + }, + { + Mach: 1.25, + CD: 0.6518 + }, + { + Mach: 1.30, + CD: 0.6589 + }, + { + Mach: 1.35, + CD: 0.6621 + }, + { + Mach: 1.40, + CD: 0.6625 + }, + { + Mach: 1.45, + CD: 0.6607 + }, + { + Mach: 1.50, + CD: 0.6573 + }, + { + Mach: 1.55, + CD: 0.6528 + }, + { + Mach: 1.60, + CD: 0.6474 + }, + { + Mach: 1.65, + CD: 0.6413 + }, + { + Mach: 1.70, + CD: 0.6347 + }, + { + Mach: 1.75, + CD: 0.6280 + }, + { + Mach: 1.80, + CD: 0.6210 + }, + { + Mach: 1.85, + CD: 0.6141 + }, + { + Mach: 1.90, + CD: 0.6072 + }, + { + Mach: 1.95, + CD: 0.6003 + }, + { + Mach: 2.00, + CD: 0.5934 + }, + { + Mach: 2.05, + CD: 0.5867 + }, + { + Mach: 2.10, + CD: 0.5804 + }, + { + Mach: 2.15, + CD: 0.5743 + }, + { + Mach: 2.20, + CD: 0.5685 + }, + { + Mach: 2.25, + CD: 0.5630 + }, + { + Mach: 2.30, + CD: 0.5577 + }, + { + Mach: 2.35, + CD: 0.5527 + }, + { + Mach: 2.40, + CD: 0.5481 + }, + { + Mach: 2.45, + CD: 0.5438 + }, + { + Mach: 2.50, + CD: 0.5397 + }, + { + Mach: 2.60, + CD: 0.5325 + }, + { + Mach: 2.70, + CD: 0.5264 + }, + { + Mach: 2.80, + CD: 0.5211 + }, + { + Mach: 2.90, + CD: 0.5168 + }, + { + Mach: 3.00, + CD: 0.5133 + }, + { + Mach: 3.10, + CD: 0.5105 + }, + { + Mach: 3.20, + CD: 0.5084 + }, + { + Mach: 3.30, + CD: 0.5067 + }, + { + Mach: 3.40, + CD: 0.5054 + }, + { + Mach: 3.50, + CD: 0.5040 + }, + { + Mach: 3.60, + CD: 0.5030 + }, + { + Mach: 3.70, + CD: 0.5022 + }, + { + Mach: 3.80, + CD: 0.5016 + }, + { + Mach: 3.90, + CD: 0.5010 + }, + { + Mach: 4.00, + CD: 0.5006 + }, + { + Mach: 4.20, + CD: 0.4998 + }, + { + Mach: 4.40, + CD: 0.4995 + }, + { + Mach: 4.60, + CD: 0.4992 + }, + { + Mach: 4.80, + CD: 0.4990 + }, + { + Mach: 5.00, + CD: 0.4988 + } + ], + G7: [ + { + Mach: 0.00, + CD: 0.1198 + }, + { + Mach: 0.05, + CD: 0.1197 + }, + { + Mach: 0.10, + CD: 0.1196 + }, + { + Mach: 0.15, + CD: 0.1194 + }, + { + Mach: 0.20, + CD: 0.1193 + }, + { + Mach: 0.25, + CD: 0.1194 + }, + { + Mach: 0.30, + CD: 0.1194 + }, + { + Mach: 0.35, + CD: 0.1194 + }, + { + Mach: 0.40, + CD: 0.1193 + }, + { + Mach: 0.45, + CD: 0.1193 + }, + { + Mach: 0.50, + CD: 0.1194 + }, + { + Mach: 0.55, + CD: 0.1193 + }, + { + Mach: 0.60, + CD: 0.1194 + }, + { + Mach: 0.65, + CD: 0.1197 + }, + { + Mach: 0.70, + CD: 0.1202 + }, + { + Mach: 0.725, + CD: 0.1207 + }, + { + Mach: 0.75, + CD: 0.1215 + }, + { + Mach: 0.775, + CD: 0.1226 + }, + { + Mach: 0.80, + CD: 0.1242 + }, + { + Mach: 0.825, + CD: 0.1266 + }, + { + Mach: 0.85, + CD: 0.1306 + }, + { + Mach: 0.875, + CD: 0.1368 + }, + { + Mach: 0.90, + CD: 0.1464 + }, + { + Mach: 0.925, + CD: 0.1660 + }, + { + Mach: 0.95, + CD: 0.2054 + }, + { + Mach: 0.975, + CD: 0.2993 + }, + { + Mach: 1.0, + CD: 0.3803 + }, + { + Mach: 1.025, + CD: 0.4015 + }, + { + Mach: 1.05, + CD: 0.4043 + }, + { + Mach: 1.075, + CD: 0.4034 + }, + { + Mach: 1.10, + CD: 0.4014 + }, + { + Mach: 1.125, + CD: 0.3987 + }, + { + Mach: 1.15, + CD: 0.3955 + }, + { + Mach: 1.20, + CD: 0.3884 + }, + { + Mach: 1.25, + CD: 0.3810 + }, + { + Mach: 1.30, + CD: 0.3732 + }, + { + Mach: 1.35, + CD: 0.3657 + }, + { + Mach: 1.40, + CD: 0.3580 + }, + { + Mach: 1.50, + CD: 0.3440 + }, + { + Mach: 1.55, + CD: 0.3376 + }, + { + Mach: 1.60, + CD: 0.3315 + }, + { + Mach: 1.65, + CD: 0.3260 + }, + { + Mach: 1.70, + CD: 0.3209 + }, + { + Mach: 1.75, + CD: 0.3160 + }, + { + Mach: 1.80, + CD: 0.3117 + }, + { + Mach: 1.85, + CD: 0.3078 + }, + { + Mach: 1.90, + CD: 0.3042 + }, + { + Mach: 1.95, + CD: 0.3010 + }, + { + Mach: 2.00, + CD: 0.2980 + }, + { + Mach: 2.05, + CD: 0.2951 + }, + { + Mach: 2.10, + CD: 0.2922 + }, + { + Mach: 2.15, + CD: 0.2892 + }, + { + Mach: 2.20, + CD: 0.2864 + }, + { + Mach: 2.25, + CD: 0.2835 + }, + { + Mach: 2.30, + CD: 0.2807 + }, + { + Mach: 2.35, + CD: 0.2779 + }, + { + Mach: 2.40, + CD: 0.2752 + }, + { + Mach: 2.45, + CD: 0.2725 + }, + { + Mach: 2.50, + CD: 0.2697 + }, + { + Mach: 2.55, + CD: 0.2670 + }, + { + Mach: 2.60, + CD: 0.2643 + }, + { + Mach: 2.65, + CD: 0.2615 + }, + { + Mach: 2.70, + CD: 0.2588 + }, + { + Mach: 2.75, + CD: 0.2561 + }, + { + Mach: 2.80, + CD: 0.2533 + }, + { + Mach: 2.85, + CD: 0.2506 + }, + { + Mach: 2.90, + CD: 0.2479 + }, + { + Mach: 2.95, + CD: 0.2451 + }, + { + Mach: 3.00, + CD: 0.2424 + }, + { + Mach: 3.10, + CD: 0.2368 + }, + { + Mach: 3.20, + CD: 0.2313 + }, + { + Mach: 3.30, + CD: 0.2258 + }, + { + Mach: 3.40, + CD: 0.2205 + }, + { + Mach: 3.50, + CD: 0.2154 + }, + { + Mach: 3.60, + CD: 0.2106 + }, + { + Mach: 3.70, + CD: 0.2060 + }, + { + Mach: 3.80, + CD: 0.2017 + }, + { + Mach: 3.90, + CD: 0.1975 + }, + { + Mach: 4.00, + CD: 0.1935 + }, + { + Mach: 4.20, + CD: 0.1861 + }, + { + Mach: 4.40, + CD: 0.1793 + }, + { + Mach: 4.60, + CD: 0.1730 + }, + { + Mach: 4.80, + CD: 0.1672 + }, + { + Mach: 5.00, + CD: 0.1618 + } + ], + G2: [ + { + Mach: 0.00, + CD: 0.2303 + }, + { + Mach: 0.05, + CD: 0.2298 + }, + { + Mach: 0.10, + CD: 0.2287 + }, + { + Mach: 0.15, + CD: 0.2271 + }, + { + Mach: 0.20, + CD: 0.2251 + }, + { + Mach: 0.25, + CD: 0.2227 + }, + { + Mach: 0.30, + CD: 0.2196 + }, + { + Mach: 0.35, + CD: 0.2156 + }, + { + Mach: 0.40, + CD: 0.2107 + }, + { + Mach: 0.45, + CD: 0.2048 + }, + { + Mach: 0.50, + CD: 0.1980 + }, + { + Mach: 0.55, + CD: 0.1905 + }, + { + Mach: 0.60, + CD: 0.1828 + }, + { + Mach: 0.65, + CD: 0.1758 + }, + { + Mach: 0.70, + CD: 0.1702 + }, + { + Mach: 0.75, + CD: 0.1669 + }, + { + Mach: 0.775, + CD: 0.1664 + }, + { + Mach: 0.80, + CD: 0.1667 + }, + { + Mach: 0.825, + CD: 0.1682 + }, + { + Mach: 0.85, + CD: 0.1711 + }, + { + Mach: 0.875, + CD: 0.1761 + }, + { + Mach: 0.90, + CD: 0.1831 + }, + { + Mach: 0.925, + CD: 0.2004 + }, + { + Mach: 0.95, + CD: 0.2589 + }, + { + Mach: 0.975, + CD: 0.3492 + }, + { + Mach: 1.0, + CD: 0.3983 + }, + { + Mach: 1.025, + CD: 0.4075 + }, + { + Mach: 1.05, + CD: 0.4103 + }, + { + Mach: 1.075, + CD: 0.4114 + }, + { + Mach: 1.10, + CD: 0.4106 + }, + { + Mach: 1.125, + CD: 0.4089 + }, + { + Mach: 1.15, + CD: 0.4068 + }, + { + Mach: 1.175, + CD: 0.4046 + }, + { + Mach: 1.20, + CD: 0.4021 + }, + { + Mach: 1.25, + CD: 0.3966 + }, + { + Mach: 1.30, + CD: 0.3904 + }, + { + Mach: 1.35, + CD: 0.3835 + }, + { + Mach: 1.40, + CD: 0.3759 + }, + { + Mach: 1.45, + CD: 0.3678 + }, + { + Mach: 1.50, + CD: 0.3594 + }, + { + Mach: 1.55, + CD: 0.3512 + }, + { + Mach: 1.60, + CD: 0.3432 + }, + { + Mach: 1.65, + CD: 0.3356 + }, + { + Mach: 1.70, + CD: 0.3282 + }, + { + Mach: 1.75, + CD: 0.3213 + }, + { + Mach: 1.80, + CD: 0.3149 + }, + { + Mach: 1.85, + CD: 0.3089 + }, + { + Mach: 1.90, + CD: 0.3033 + }, + { + Mach: 1.95, + CD: 0.2982 + }, + { + Mach: 2.00, + CD: 0.2933 + }, + { + Mach: 2.05, + CD: 0.2889 + }, + { + Mach: 2.10, + CD: 0.2846 + }, + { + Mach: 2.15, + CD: 0.2806 + }, + { + Mach: 2.20, + CD: 0.2768 + }, + { + Mach: 2.25, + CD: 0.2731 + }, + { + Mach: 2.30, + CD: 0.2696 + }, + { + Mach: 2.35, + CD: 0.2663 + }, + { + Mach: 2.40, + CD: 0.2632 + }, + { + Mach: 2.45, + CD: 0.2602 + }, + { + Mach: 2.50, + CD: 0.2572 + }, + { + Mach: 2.55, + CD: 0.2543 + }, + { + Mach: 2.60, + CD: 0.2515 + }, + { + Mach: 2.65, + CD: 0.2487 + }, + { + Mach: 2.70, + CD: 0.2460 + }, + { + Mach: 2.75, + CD: 0.2433 + }, + { + Mach: 2.80, + CD: 0.2408 + }, + { + Mach: 2.85, + CD: 0.2382 + }, + { + Mach: 2.90, + CD: 0.2357 + }, + { + Mach: 2.95, + CD: 0.2333 + }, + { + Mach: 3.00, + CD: 0.2309 + }, + { + Mach: 3.10, + CD: 0.2262 + }, + { + Mach: 3.20, + CD: 0.2217 + }, + { + Mach: 3.30, + CD: 0.2173 + }, + { + Mach: 3.40, + CD: 0.2132 + }, + { + Mach: 3.50, + CD: 0.2091 + }, + { + Mach: 3.60, + CD: 0.2052 + }, + { + Mach: 3.70, + CD: 0.2014 + }, + { + Mach: 3.80, + CD: 0.1978 + }, + { + Mach: 3.90, + CD: 0.1944 + }, + { + Mach: 4.00, + CD: 0.1912 + }, + { + Mach: 4.20, + CD: 0.1851 + }, + { + Mach: 4.40, + CD: 0.1794 + }, + { + Mach: 4.60, + CD: 0.1741 + }, + { + Mach: 4.80, + CD: 0.1693 + }, + { + Mach: 5.00, + CD: 0.1648 + } + ], + G5: [ + { + Mach: 0.00, + CD: 0.1710 + }, + { + Mach: 0.05, + CD: 0.1719 + }, + { + Mach: 0.10, + CD: 0.1727 + }, + { + Mach: 0.15, + CD: 0.1732 + }, + { + Mach: 0.20, + CD: 0.1734 + }, + { + Mach: 0.25, + CD: 0.1730 + }, + { + Mach: 0.30, + CD: 0.1718 + }, + { + Mach: 0.35, + CD: 0.1696 + }, + { + Mach: 0.40, + CD: 0.1668 + }, + { + Mach: 0.45, + CD: 0.1637 + }, + { + Mach: 0.50, + CD: 0.1603 + }, + { + Mach: 0.55, + CD: 0.1566 + }, + { + Mach: 0.60, + CD: 0.1529 + }, + { + Mach: 0.65, + CD: 0.1497 + }, + { + Mach: 0.70, + CD: 0.1473 + }, + { + Mach: 0.75, + CD: 0.1463 + }, + { + Mach: 0.80, + CD: 0.1489 + }, + { + Mach: 0.85, + CD: 0.1583 + }, + { + Mach: 0.875, + CD: 0.1672 + }, + { + Mach: 0.90, + CD: 0.1815 + }, + { + Mach: 0.925, + CD: 0.2051 + }, + { + Mach: 0.95, + CD: 0.2413 + }, + { + Mach: 0.975, + CD: 0.2884 + }, + { + Mach: 1.0, + CD: 0.3379 + }, + { + Mach: 1.025, + CD: 0.3785 + }, + { + Mach: 1.05, + CD: 0.4032 + }, + { + Mach: 1.075, + CD: 0.4147 + }, + { + Mach: 1.10, + CD: 0.4201 + }, + { + Mach: 1.15, + CD: 0.4278 + }, + { + Mach: 1.20, + CD: 0.4338 + }, + { + Mach: 1.25, + CD: 0.4373 + }, + { + Mach: 1.30, + CD: 0.4392 + }, + { + Mach: 1.35, + CD: 0.4403 + }, + { + Mach: 1.40, + CD: 0.4406 + }, + { + Mach: 1.45, + CD: 0.4401 + }, + { + Mach: 1.50, + CD: 0.4386 + }, + { + Mach: 1.55, + CD: 0.4362 + }, + { + Mach: 1.60, + CD: 0.4328 + }, + { + Mach: 1.65, + CD: 0.4286 + }, + { + Mach: 1.70, + CD: 0.4237 + }, + { + Mach: 1.75, + CD: 0.4182 + }, + { + Mach: 1.80, + CD: 0.4121 + }, + { + Mach: 1.85, + CD: 0.4057 + }, + { + Mach: 1.90, + CD: 0.3991 + }, + { + Mach: 1.95, + CD: 0.3926 + }, + { + Mach: 2.00, + CD: 0.3861 + }, + { + Mach: 2.05, + CD: 0.3800 + }, + { + Mach: 2.10, + CD: 0.3741 + }, + { + Mach: 2.15, + CD: 0.3684 + }, + { + Mach: 2.20, + CD: 0.3630 + }, + { + Mach: 2.25, + CD: 0.3578 + }, + { + Mach: 2.30, + CD: 0.3529 + }, + { + Mach: 2.35, + CD: 0.3481 + }, + { + Mach: 2.40, + CD: 0.3435 + }, + { + Mach: 2.45, + CD: 0.3391 + }, + { + Mach: 2.50, + CD: 0.3349 + }, + { + Mach: 2.60, + CD: 0.3269 + }, + { + Mach: 2.70, + CD: 0.3194 + }, + { + Mach: 2.80, + CD: 0.3125 + }, + { + Mach: 2.90, + CD: 0.3060 + }, + { + Mach: 3.00, + CD: 0.2999 + }, + { + Mach: 3.10, + CD: 0.2942 + }, + { + Mach: 3.20, + CD: 0.2889 + }, + { + Mach: 3.30, + CD: 0.2838 + }, + { + Mach: 3.40, + CD: 0.2790 + }, + { + Mach: 3.50, + CD: 0.2745 + }, + { + Mach: 3.60, + CD: 0.2703 + }, + { + Mach: 3.70, + CD: 0.2662 + }, + { + Mach: 3.80, + CD: 0.2624 + }, + { + Mach: 3.90, + CD: 0.2588 + }, + { + Mach: 4.00, + CD: 0.2553 + }, + { + Mach: 4.20, + CD: 0.2488 + }, + { + Mach: 4.40, + CD: 0.2429 + }, + { + Mach: 4.60, + CD: 0.2376 + }, + { + Mach: 4.80, + CD: 0.2326 + }, + { + Mach: 5.00, + CD: 0.2280 + } + ], + G6: [ + { + Mach: 0.00, + CD: 0.2617 + }, + { + Mach: 0.05, + CD: 0.2553 + }, + { + Mach: 0.10, + CD: 0.2491 + }, + { + Mach: 0.15, + CD: 0.2432 + }, + { + Mach: 0.20, + CD: 0.2376 + }, + { + Mach: 0.25, + CD: 0.2324 + }, + { + Mach: 0.30, + CD: 0.2278 + }, + { + Mach: 0.35, + CD: 0.2238 + }, + { + Mach: 0.40, + CD: 0.2205 + }, + { + Mach: 0.45, + CD: 0.2177 + }, + { + Mach: 0.50, + CD: 0.2155 + }, + { + Mach: 0.55, + CD: 0.2138 + }, + { + Mach: 0.60, + CD: 0.2126 + }, + { + Mach: 0.65, + CD: 0.2121 + }, + { + Mach: 0.70, + CD: 0.2122 + }, + { + Mach: 0.75, + CD: 0.2132 + }, + { + Mach: 0.80, + CD: 0.2154 + }, + { + Mach: 0.85, + CD: 0.2194 + }, + { + Mach: 0.875, + CD: 0.2229 + }, + { + Mach: 0.90, + CD: 0.2297 + }, + { + Mach: 0.925, + CD: 0.2449 + }, + { + Mach: 0.95, + CD: 0.2732 + }, + { + Mach: 0.975, + CD: 0.3141 + }, + { + Mach: 1.0, + CD: 0.3597 + }, + { + Mach: 1.025, + CD: 0.3994 + }, + { + Mach: 1.05, + CD: 0.4261 + }, + { + Mach: 1.075, + CD: 0.4402 + }, + { + Mach: 1.10, + CD: 0.4465 + }, + { + Mach: 1.125, + CD: 0.4490 + }, + { + Mach: 1.15, + CD: 0.4497 + }, + { + Mach: 1.175, + CD: 0.4494 + }, + { + Mach: 1.20, + CD: 0.4482 + }, + { + Mach: 1.225, + CD: 0.4464 + }, + { + Mach: 1.25, + CD: 0.4441 + }, + { + Mach: 1.30, + CD: 0.4390 + }, + { + Mach: 1.35, + CD: 0.4336 + }, + { + Mach: 1.40, + CD: 0.4279 + }, + { + Mach: 1.45, + CD: 0.4221 + }, + { + Mach: 1.50, + CD: 0.4162 + }, + { + Mach: 1.55, + CD: 0.4102 + }, + { + Mach: 1.60, + CD: 0.4042 + }, + { + Mach: 1.65, + CD: 0.3981 + }, + { + Mach: 1.70, + CD: 0.3919 + }, + { + Mach: 1.75, + CD: 0.3855 + }, + { + Mach: 1.80, + CD: 0.3788 + }, + { + Mach: 1.85, + CD: 0.3721 + }, + { + Mach: 1.90, + CD: 0.3652 + }, + { + Mach: 1.95, + CD: 0.3583 + }, + { + Mach: 2.00, + CD: 0.3515 + }, + { + Mach: 2.05, + CD: 0.3447 + }, + { + Mach: 2.10, + CD: 0.3381 + }, + { + Mach: 2.15, + CD: 0.3314 + }, + { + Mach: 2.20, + CD: 0.3249 + }, + { + Mach: 2.25, + CD: 0.3185 + }, + { + Mach: 2.30, + CD: 0.3122 + }, + { + Mach: 2.35, + CD: 0.3060 + }, + { + Mach: 2.40, + CD: 0.3000 + }, + { + Mach: 2.45, + CD: 0.2941 + }, + { + Mach: 2.50, + CD: 0.2883 + }, + { + Mach: 2.60, + CD: 0.2772 + }, + { + Mach: 2.70, + CD: 0.2668 + }, + { + Mach: 2.80, + CD: 0.2574 + }, + { + Mach: 2.90, + CD: 0.2487 + }, + { + Mach: 3.00, + CD: 0.2407 + }, + { + Mach: 3.10, + CD: 0.2333 + }, + { + Mach: 3.20, + CD: 0.2265 + }, + { + Mach: 3.30, + CD: 0.2202 + }, + { + Mach: 3.40, + CD: 0.2144 + }, + { + Mach: 3.50, + CD: 0.2089 + }, + { + Mach: 3.60, + CD: 0.2039 + }, + { + Mach: 3.70, + CD: 0.1991 + }, + { + Mach: 3.80, + CD: 0.1947 + }, + { + Mach: 3.90, + CD: 0.1905 + }, + { + Mach: 4.00, + CD: 0.1866 + }, + { + Mach: 4.20, + CD: 0.1794 + }, + { + Mach: 4.40, + CD: 0.1730 + }, + { + Mach: 4.60, + CD: 0.1673 + }, + { + Mach: 4.80, + CD: 0.1621 + }, + { + Mach: 5.00, + CD: 0.1574 + } + ], + G8: [ + { + Mach: 0.00, + CD: 0.2105 + }, + { + Mach: 0.05, + CD: 0.2105 + }, + { + Mach: 0.10, + CD: 0.2104 + }, + { + Mach: 0.15, + CD: 0.2104 + }, + { + Mach: 0.20, + CD: 0.2103 + }, + { + Mach: 0.25, + CD: 0.2103 + }, + { + Mach: 0.30, + CD: 0.2103 + }, + { + Mach: 0.35, + CD: 0.2103 + }, + { + Mach: 0.40, + CD: 0.2103 + }, + { + Mach: 0.45, + CD: 0.2102 + }, + { + Mach: 0.50, + CD: 0.2102 + }, + { + Mach: 0.55, + CD: 0.2102 + }, + { + Mach: 0.60, + CD: 0.2102 + }, + { + Mach: 0.65, + CD: 0.2102 + }, + { + Mach: 0.70, + CD: 0.2103 + }, + { + Mach: 0.75, + CD: 0.2103 + }, + { + Mach: 0.80, + CD: 0.2104 + }, + { + Mach: 0.825, + CD: 0.2104 + }, + { + Mach: 0.85, + CD: 0.2105 + }, + { + Mach: 0.875, + CD: 0.2106 + }, + { + Mach: 0.90, + CD: 0.2109 + }, + { + Mach: 0.925, + CD: 0.2183 + }, + { + Mach: 0.95, + CD: 0.2571 + }, + { + Mach: 0.975, + CD: 0.3358 + }, + { + Mach: 1.0, + CD: 0.4068 + }, + { + Mach: 1.025, + CD: 0.4378 + }, + { + Mach: 1.05, + CD: 0.4476 + }, + { + Mach: 1.075, + CD: 0.4493 + }, + { + Mach: 1.10, + CD: 0.4477 + }, + { + Mach: 1.125, + CD: 0.4450 + }, + { + Mach: 1.15, + CD: 0.4419 + }, + { + Mach: 1.20, + CD: 0.4353 + }, + { + Mach: 1.25, + CD: 0.4283 + }, + { + Mach: 1.30, + CD: 0.4208 + }, + { + Mach: 1.35, + CD: 0.4133 + }, + { + Mach: 1.40, + CD: 0.4059 + }, + { + Mach: 1.45, + CD: 0.3986 + }, + { + Mach: 1.50, + CD: 0.3915 + }, + { + Mach: 1.55, + CD: 0.3845 + }, + { + Mach: 1.60, + CD: 0.3777 + }, + { + Mach: 1.65, + CD: 0.3710 + }, + { + Mach: 1.70, + CD: 0.3645 + }, + { + Mach: 1.75, + CD: 0.3581 + }, + { + Mach: 1.80, + CD: 0.3519 + }, + { + Mach: 1.85, + CD: 0.3458 + }, + { + Mach: 1.90, + CD: 0.3400 + }, + { + Mach: 1.95, + CD: 0.3343 + }, + { + Mach: 2.00, + CD: 0.3288 + }, + { + Mach: 2.05, + CD: 0.3234 + }, + { + Mach: 2.10, + CD: 0.3182 + }, + { + Mach: 2.15, + CD: 0.3131 + }, + { + Mach: 2.20, + CD: 0.3081 + }, + { + Mach: 2.25, + CD: 0.3032 + }, + { + Mach: 2.30, + CD: 0.2983 + }, + { + Mach: 2.35, + CD: 0.2937 + }, + { + Mach: 2.40, + CD: 0.2891 + }, + { + Mach: 2.45, + CD: 0.2845 + }, + { + Mach: 2.50, + CD: 0.2802 + }, + { + Mach: 2.60, + CD: 0.2720 + }, + { + Mach: 2.70, + CD: 0.2642 + }, + { + Mach: 2.80, + CD: 0.2569 + }, + { + Mach: 2.90, + CD: 0.2499 + }, + { + Mach: 3.00, + CD: 0.2432 + }, + { + Mach: 3.10, + CD: 0.2368 + }, + { + Mach: 3.20, + CD: 0.2308 + }, + { + Mach: 3.30, + CD: 0.2251 + }, + { + Mach: 3.40, + CD: 0.2197 + }, + { + Mach: 3.50, + CD: 0.2147 + }, + { + Mach: 3.60, + CD: 0.2101 + }, + { + Mach: 3.70, + CD: 0.2058 + }, + { + Mach: 3.80, + CD: 0.2019 + }, + { + Mach: 3.90, + CD: 0.1983 + }, + { + Mach: 4.00, + CD: 0.1950 + }, + { + Mach: 4.20, + CD: 0.1890 + }, + { + Mach: 4.40, + CD: 0.1837 + }, + { + Mach: 4.60, + CD: 0.1791 + }, + { + Mach: 4.80, + CD: 0.1750 + }, + { + Mach: 5.00, + CD: 0.1713 + } + ], + GI: [ + { + Mach: 0.00, + CD: 0.2282 + }, + { + Mach: 0.05, + CD: 0.2282 + }, + { + Mach: 0.10, + CD: 0.2282 + }, + { + Mach: 0.15, + CD: 0.2282 + }, + { + Mach: 0.20, + CD: 0.2282 + }, + { + Mach: 0.25, + CD: 0.2282 + }, + { + Mach: 0.30, + CD: 0.2282 + }, + { + Mach: 0.35, + CD: 0.2282 + }, + { + Mach: 0.40, + CD: 0.2282 + }, + { + Mach: 0.45, + CD: 0.2282 + }, + { + Mach: 0.50, + CD: 0.2282 + }, + { + Mach: 0.55, + CD: 0.2282 + }, + { + Mach: 0.60, + CD: 0.2282 + }, + { + Mach: 0.65, + CD: 0.2282 + }, + { + Mach: 0.70, + CD: 0.2282 + }, + { + Mach: 0.725, + CD: 0.2353 + }, + { + Mach: 0.75, + CD: 0.2434 + }, + { + Mach: 0.775, + CD: 0.2515 + }, + { + Mach: 0.80, + CD: 0.2596 + }, + { + Mach: 0.825, + CD: 0.2677 + }, + { + Mach: 0.85, + CD: 0.2759 + }, + { + Mach: 0.875, + CD: 0.2913 + }, + { + Mach: 0.90, + CD: 0.3170 + }, + { + Mach: 0.925, + CD: 0.3442 + }, + { + Mach: 0.95, + CD: 0.3728 + }, + { + Mach: 1.0, + CD: 0.4349 + }, + { + Mach: 1.05, + CD: 0.5034 + }, + { + Mach: 1.075, + CD: 0.5402 + }, + { + Mach: 1.10, + CD: 0.5756 + }, + { + Mach: 1.125, + CD: 0.5887 + }, + { + Mach: 1.15, + CD: 0.6018 + }, + { + Mach: 1.175, + CD: 0.6149 + }, + { + Mach: 1.20, + CD: 0.6279 + }, + { + Mach: 1.225, + CD: 0.6418 + }, + { + Mach: 1.25, + CD: 0.6423 + }, + { + Mach: 1.30, + CD: 0.6423 + }, + { + Mach: 1.35, + CD: 0.6423 + }, + { + Mach: 1.40, + CD: 0.6423 + }, + { + Mach: 1.45, + CD: 0.6423 + }, + { + Mach: 1.50, + CD: 0.6423 + }, + { + Mach: 1.55, + CD: 0.6423 + }, + { + Mach: 1.60, + CD: 0.6423 + }, + { + Mach: 1.625, + CD: 0.6407 + }, + { + Mach: 1.65, + CD: 0.6378 + }, + { + Mach: 1.70, + CD: 0.6321 + }, + { + Mach: 1.75, + CD: 0.6266 + }, + { + Mach: 1.80, + CD: 0.6213 + }, + { + Mach: 1.85, + CD: 0.6163 + }, + { + Mach: 1.90, + CD: 0.6113 + }, + { + Mach: 1.95, + CD: 0.6066 + }, + { + Mach: 2.00, + CD: 0.6020 + }, + { + Mach: 2.05, + CD: 0.5976 + }, + { + Mach: 2.10, + CD: 0.5933 + }, + { + Mach: 2.15, + CD: 0.5891 + }, + { + Mach: 2.20, + CD: 0.5850 + }, + { + Mach: 2.25, + CD: 0.5811 + }, + { + Mach: 2.30, + CD: 0.5773 + }, + { + Mach: 2.35, + CD: 0.5733 + }, + { + Mach: 2.40, + CD: 0.5679 + }, + { + Mach: 2.45, + CD: 0.5626 + }, + { + Mach: 2.50, + CD: 0.5576 + }, + { + Mach: 2.60, + CD: 0.5478 + }, + { + Mach: 2.70, + CD: 0.5386 + }, + { + Mach: 2.80, + CD: 0.5298 + }, + { + Mach: 2.90, + CD: 0.5215 + }, + { + Mach: 3.00, + CD: 0.5136 + }, + { + Mach: 3.10, + CD: 0.5061 + }, + { + Mach: 3.20, + CD: 0.4989 + }, + { + Mach: 3.30, + CD: 0.4921 + }, + { + Mach: 3.40, + CD: 0.4855 + }, + { + Mach: 3.50, + CD: 0.4792 + }, + { + Mach: 3.60, + CD: 0.4732 + }, + { + Mach: 3.70, + CD: 0.4674 + }, + { + Mach: 3.80, + CD: 0.4618 + }, + { + Mach: 3.90, + CD: 0.4564 + }, + { + Mach: 4.00, + CD: 0.4513 + }, + { + Mach: 4.20, + CD: 0.4415 + }, + { + Mach: 4.40, + CD: 0.4323 + }, + { + Mach: 4.60, + CD: 0.4238 + }, + { + Mach: 4.80, + CD: 0.4157 + }, + { + Mach: 5.00, + CD: 0.4082 + } + ], + GS: [ + { + Mach: 0.00, + CD: 0.4662 + }, + { + Mach: 0.05, + CD: 0.4689 + }, + { + Mach: 0.10, + CD: 0.4717 + }, + { + Mach: 0.15, + CD: 0.4745 + }, + { + Mach: 0.20, + CD: 0.4772 + }, + { + Mach: 0.25, + CD: 0.4800 + }, + { + Mach: 0.30, + CD: 0.4827 + }, + { + Mach: 0.35, + CD: 0.4852 + }, + { + Mach: 0.40, + CD: 0.4882 + }, + { + Mach: 0.45, + CD: 0.4920 + }, + { + Mach: 0.50, + CD: 0.4970 + }, + { + Mach: 0.55, + CD: 0.5080 + }, + { + Mach: 0.60, + CD: 0.5260 + }, + { + Mach: 0.65, + CD: 0.5590 + }, + { + Mach: 0.70, + CD: 0.5920 + }, + { + Mach: 0.75, + CD: 0.6258 + }, + { + Mach: 0.80, + CD: 0.6610 + }, + { + Mach: 0.85, + CD: 0.6985 + }, + { + Mach: 0.90, + CD: 0.7370 + }, + { + Mach: 0.95, + CD: 0.7757 + }, + { + Mach: 1.0, + CD: 0.8140 + }, + { + Mach: 1.05, + CD: 0.8512 + }, + { + Mach: 1.10, + CD: 0.8870 + }, + { + Mach: 1.15, + CD: 0.9210 + }, + { + Mach: 1.20, + CD: 0.9510 + }, + { + Mach: 1.25, + CD: 0.9740 + }, + { + Mach: 1.30, + CD: 0.9910 + }, + { + Mach: 1.35, + CD: 0.9990 + }, + { + Mach: 1.40, + CD: 1.0030 + }, + { + Mach: 1.45, + CD: 1.0060 + }, + { + Mach: 1.50, + CD: 1.0080 + }, + { + Mach: 1.55, + CD: 1.0090 + }, + { + Mach: 1.60, + CD: 1.0090 + }, + { + Mach: 1.65, + CD: 1.0090 + }, + { + Mach: 1.70, + CD: 1.0090 + }, + { + Mach: 1.75, + CD: 1.0080 + }, + { + Mach: 1.80, + CD: 1.0070 + }, + { + Mach: 1.85, + CD: 1.0060 + }, + { + Mach: 1.90, + CD: 1.0040 + }, + { + Mach: 1.95, + CD: 1.0025 + }, + { + Mach: 2.00, + CD: 1.0010 + }, + { + Mach: 2.05, + CD: 0.9990 + }, + { + Mach: 2.10, + CD: 0.9970 + }, + { + Mach: 2.15, + CD: 0.9956 + }, + { + Mach: 2.20, + CD: 0.9940 + }, + { + Mach: 2.25, + CD: 0.9916 + }, + { + Mach: 2.30, + CD: 0.9890 + }, + { + Mach: 2.35, + CD: 0.9869 + }, + { + Mach: 2.40, + CD: 0.9850 + }, + { + Mach: 2.45, + CD: 0.9830 + }, + { + Mach: 2.50, + CD: 0.9810 + }, + { + Mach: 2.55, + CD: 0.9790 + }, + { + Mach: 2.60, + CD: 0.9770 + }, + { + Mach: 2.65, + CD: 0.9750 + }, + { + Mach: 2.70, + CD: 0.9730 + }, + { + Mach: 2.75, + CD: 0.9710 + }, + { + Mach: 2.80, + CD: 0.9690 + }, + { + Mach: 2.85, + CD: 0.9670 + }, + { + Mach: 2.90, + CD: 0.9650 + }, + { + Mach: 2.95, + CD: 0.9630 + }, + { + Mach: 3.00, + CD: 0.9610 + }, + { + Mach: 3.05, + CD: 0.9589 + }, + { + Mach: 3.10, + CD: 0.9570 + }, + { + Mach: 3.15, + CD: 0.9555 + }, + { + Mach: 3.20, + CD: 0.9540 + }, + { + Mach: 3.25, + CD: 0.9520 + }, + { + Mach: 3.30, + CD: 0.9500 + }, + { + Mach: 3.35, + CD: 0.9485 + }, + { + Mach: 3.40, + CD: 0.9470 + }, + { + Mach: 3.45, + CD: 0.9450 + }, + { + Mach: 3.50, + CD: 0.9430 + }, + { + Mach: 3.55, + CD: 0.9414 + }, + { + Mach: 3.60, + CD: 0.9400 + }, + { + Mach: 3.65, + CD: 0.9385 + }, + { + Mach: 3.70, + CD: 0.9370 + }, + { + Mach: 3.75, + CD: 0.9355 + }, + { + Mach: 3.80, + CD: 0.9340 + }, + { + Mach: 3.85, + CD: 0.9325 + }, + { + Mach: 3.90, + CD: 0.9310 + }, + { + Mach: 3.95, + CD: 0.9295 + }, + { + Mach: 4.00, + CD: 0.9280 + } + ] +} + +export default Table diff --git a/src/v2/index.js b/src/v2/index.js new file mode 100644 index 0000000..398a634 --- /dev/null +++ b/src/v2/index.js @@ -0,0 +1,76 @@ +import { + AbstractUnit, Angular, Distance, Velocity, Weight, Temperature, Pressure, Energy, + Unit, UnitProps, unitTypeCoerce, UNew, Measure, preferredUnits +} from "./unit"; + +import Table from "./drag_tables.js"; + +import Vector from "./vector"; + +import { + Atmo, + Wind, + Shot, + cStandardDensity, + cStandardTemperature, + cStandardPressure, + cIcaoStandardHumidity, + cSpeedOfSound +} from "./conditions"; +import { Weapon, Ammo } from "./munition"; +import DragModel, { DragDataPoint, DragTable, BCPoint, DragModelMultiBC } from "./drag_model"; + +import { + TrajectoryData, + TrajFlag, + DangerSpace, + HitResult +} from "./trajectory_data" + +import TrajectoryCalc, { + getGlobalMaxCalcStepSize, + getGlobalUsePowderSensitivity, + setGlobalMaxCalcStepSize, + setGlobalUsePowderSensitivity, + resetGlobals +} from "./trajectory_calc"; +import Calculator from "./interface"; + +export { + AbstractUnit, Angular, Distance, Velocity, Weight, Temperature, Pressure, Energy, + Unit, UnitProps, unitTypeCoerce, UNew, Measure, preferredUnits, + + Vector, + Table, + + Atmo, + Wind, + Shot, + cStandardDensity, + cStandardTemperature, + cStandardPressure, + cIcaoStandardHumidity, + cSpeedOfSound, + + DragModel, + DragDataPoint, + DragTable, + BCPoint, + DragModelMultiBC, + + Weapon, Ammo, + + TrajectoryCalc, + getGlobalMaxCalcStepSize, + getGlobalUsePowderSensitivity, + setGlobalMaxCalcStepSize, + setGlobalUsePowderSensitivity, + resetGlobals, + + TrajectoryData, + TrajFlag, + DangerSpace, + HitResult, +} + +export default Calculator; diff --git a/src/v2/interface.ts b/src/v2/interface.ts new file mode 100644 index 0000000..105eab4 --- /dev/null +++ b/src/v2/interface.ts @@ -0,0 +1,84 @@ +import TrajectoryCalc from "./trajectory_calc"; +import { Shot } from "./conditions"; +import { DragTable } from "./drag_model"; +import { HitResult } from "./trajectory_data"; +import { UNew, Angular, Distance, unitTypeCoerce, preferredUnits } from "./unit"; + + +/** + * A class for performing calculations related to trajectories. + */ +export default class Calculator { + + protected _calc: TrajectoryCalc + + /** + * Retrieves the drag table data from the trajectory calculations. + * @returns {DragTable} - The drag table data used in the trajectory calculations. + */ + get cdm(): DragTable { + return this._calc.tableData + } + + /** + * Calculates the barrel elevation required to hit a target at a specified distance. + * @param {Object} options - Parameters for the calculation. + * @param {Shot} options.shot - The shot parameters including weapon and ammo data. + * @param {number | Distance} options.targetDistance - The distance to the target, can be a number or Distance object. + * @returns {Angular} - The required barrel elevation in radians. + */ + barrelElevationForTarget( + shot: Shot, + targetDistance: (number | Distance) + ): Angular { + this._calc = new TrajectoryCalc(shot.ammo) + const _targetDistance = unitTypeCoerce(targetDistance, Distance, preferredUnits.distance) + const totalElevation = this._calc.zeroAngle(shot, _targetDistance) + return UNew.Radian( + totalElevation.In(Angular.Radian) - shot.lookAngle.In(Angular.Radian) + ) + } + + /** + * Sets the weapon's zero elevation based on the specified zero distance. + * @param {Shot} shot - The shot parameters including weapon and ammo data. + * @param {number | Distance} zeroDistance - The distance at which the weapon should be zeroed, can be a number or Distance object. + * @returns {Angular} - The new zero elevation of the weapon in radians. + */ + setWeaponZero(shot: Shot, zeroDistance: (number | Distance)): Angular { + shot.weapon.zeroElevation = this.barrelElevationForTarget(shot, zeroDistance) + return shot.weapon.zeroElevation + } + + /** + * Fires a shot and calculates the hit result over a specified trajectory range. + * @param {Object} options - Parameters for the shot and trajectory calculation. + * @param {Shot} options.shot - The shot parameters including weapon and ammo data. + * @param {number | Distance} options.trajectoryRange - The total range of the trajectory, can be a number or Distance object. + * @param {number | Distance} [options.trajectoryStep=0] - The step size for trajectory calculations, can be a number or Distance object. Default is 0. + * @param {boolean} [options.extraData=false] - Flag indicating whether to include extra data in the result. Default is false. + * @returns {HitResult} - The result of the shot, including information about the hit. + */ + fire({ + shot, + trajectoryRange, + trajectoryStep = 0, + extraData = false, + }: { + shot: Shot, + trajectoryRange: (number | Distance), + trajectoryStep?: (number | Distance), + extraData?: boolean, + }): HitResult { + const _trajectoryRange: Distance = unitTypeCoerce(trajectoryRange, Distance, preferredUnits.distance) + + const _trajectoryStep = trajectoryStep + ? unitTypeCoerce(trajectoryStep, Distance, preferredUnits.distance) + : _trajectoryRange.In(_trajectoryRange.units) + + this._calc = new TrajectoryCalc(shot.ammo) + + const data = this._calc.trajectory(shot, _trajectoryRange, _trajectoryStep, extraData) + return new HitResult(shot, data, extraData) + } +} \ No newline at end of file diff --git a/src/v2/munition.ts b/src/v2/munition.ts new file mode 100644 index 0000000..68f7ed0 --- /dev/null +++ b/src/v2/munition.ts @@ -0,0 +1,111 @@ +// Module for Weapon and Ammo properties definitions + +// Import necessary units and settings +import { UNew, unitTypeCoerce, Distance, Angular, Temperature, Velocity, preferredUnits } from './unit'; +import DragModel from "./drag_model.js"; + + +class Weapon { + + readonly sightHeight: Distance; + readonly twist: Distance; + public zeroElevation: Angular; + + /** + * Initializes a new instance of the Weapon class. + * @param {Object} options - Parameters for initializing the weapon. + * @param {number | Distance | null} [options.sightHeight=null] - Height of the sight above the bore axis. + * @param {number | Distance | null} [options.twist=null] - The twist rate of the barrel. + * @param {number | Angular | null} [options.zeroElevation=null] - The look angle for the zero distance. + */ + constructor({ + sightHeight = null, + twist = null, + zeroElevation = null + }: { + sightHeight?: number | Distance | null; + twist?: number | Distance | null; + zeroElevation?: number | Angular | null; + } + ) { + this.sightHeight = unitTypeCoerce(sightHeight ?? 0, Distance, preferredUnits.sight_height) + this.twist = unitTypeCoerce(twist ?? 0, Distance, preferredUnits.twist) + this.zeroElevation = unitTypeCoerce(zeroElevation ?? 0, Angular, preferredUnits.angular) + } +} + + +class Ammo { + + readonly dm: DragModel; + readonly mv: Velocity; + readonly powderTemp: Temperature; + protected _tempModifier: number; + + /** + * Creates an instance of Ammo with specified properties. + * @param {Object} options - Parameters for initializing the Ammo instance. + * @param {DragModel} options.dm - Drag model instance. + * @param {number | Velocity} options.mv - Velocity value. + * @param {number} [options.tempModifier=0] - Temperature modifier value. Defaults to 0. + * @param {number | Temperature | null} [options.powderTemp=null] - Powder temperature value. Defaults to null. + */ + constructor({ + dm, + mv, + powderTemp = null, + tempModifier = 0 + }: { + dm: DragModel; + mv: number | Velocity; + powderTemp?: number | Temperature | null; + tempModifier?: number + }) { + if (!dm) { + throw new Error("'dm' have to be an instance of 'DragModel'") + } + this.dm = dm; + this.mv = unitTypeCoerce(mv ?? 0, Velocity, preferredUnits.velocity); + this.powderTemp = unitTypeCoerce(powderTemp ?? UNew.Celsius(15), Temperature, preferredUnits.temperature); + this._tempModifier = tempModifier ?? 0; + } + + /** + * Calculates the velocity correction based on the change in temperature and assigns it to the temperature modifier. + * @param {number | Velocity} otherVelocity - The velocity to compare with. + * @param {number | Temperature} otherTemperature - The temperature to compare with. + * @returns {number} - The calculated temperature sensitivity adjustment. + */ + calcPowderSens(otherVelocity: (number | Velocity), otherTemperature: (number | Temperature)): number { + const v0 = this.mv.In(Velocity.MPS) + const t0 = this.powderTemp.In(Temperature.Celsius) + const v1 = unitTypeCoerce(otherVelocity, Velocity, preferredUnits.velocity).In(Velocity.MPS) + const t1 = unitTypeCoerce(otherTemperature, Temperature, preferredUnits.temperature).In(Temperature.Celsius) + + const vDelta = Math.abs(v0 - v1) + const tDelta = Math.abs(t0 - t1) + const vLower = v1 < v0 ? v1 : v0 + + if ((vDelta === 0) || (tDelta === 0)) { + throw new Error("Temperature modifier error, other velocity and temperature can't be same as default") + } + this._tempModifier = vDelta / tDelta * (15 / vLower) // * 100 + return this._tempModifier + } + + /** + * Calculates the muzzle velocity at a given temperature based on the temperature modifier. + * @param {number | Temperature} currentTemp - The current temperature for which to calculate the velocity. + * @returns {Velocity} - The calculated muzzle velocity at the specified temperature. + */ + getVelocityForTemp(currentTemp: (number | Temperature)): Velocity { + const v0 = this.mv.In(Velocity.MPS) + const t0 = this.powderTemp.In(Temperature.Celsius) + const t1 = unitTypeCoerce(currentTemp, Temperature, preferredUnits.temperature).In(Temperature.Celsius) + const tDelta = t1 - t0 + const muzzleVelocity = this._tempModifier / (15 / v0) * tDelta + v0 + return UNew.MPS(muzzleVelocity) + } +} + +export { Weapon, Ammo }; diff --git a/src/v2/trajectory_calc.ts b/src/v2/trajectory_calc.ts new file mode 100644 index 0000000..59a893f --- /dev/null +++ b/src/v2/trajectory_calc.ts @@ -0,0 +1,623 @@ +// Conditions module +import { Atmo, Shot, Wind } from './conditions'; +// Munition module +import { Ammo, Weapon } from './munition'; +// TrajectoryData module +import { TrajectoryData, TrajFlag } from './trajectory_data'; +// Unit module +import { Angular, Distance, UNew, Weight, Pressure, Velocity, Temperature, unitTypeCoerce, preferredUnits } from './unit'; +// Vector module +import Vector from "./vector"; +import type { DragTable } from "./drag_model"; + + +// Constants +const cZeroFindingAccuracy: number = 0.000005; +const cMinimumVelocity: number = 50.0; +const cMaximumDrop: number = -15000; +const cMaxIterations: number = 20; +const cGravityConstant: number = -32.17405; + +let _globalUsePowderSensitivity = false; +let _globalMaxCalcStepSize: Distance = UNew.Foot(0.5); + +/** + * Retrieves the current global maximum calculation step size. + * @returns {Distance} - The global maximum calculation step size. + */ +function getGlobalMaxCalcStepSize(): Distance { + return _globalMaxCalcStepSize; +} + +/** + * Retrieves the current global setting for powder sensitivity usage. + * @returns {boolean} - The current global setting for powder sensitivity. + */ +function getGlobalUsePowderSensitivity(): boolean { + return _globalUsePowderSensitivity; +} + +/** + * Resets global settings to their default values. + * - `globalUsePowderSensitivity` is set to `false`. + * - `globalMaxCalcStepSize` is set to `0.5` feet. + */ +function resetGlobals(): void { + _globalUsePowderSensitivity = false; + _globalMaxCalcStepSize = UNew.Foot(0.5); +} + +/** + * Sets the global maximum calculation step size. + * @param {number | Distance} value - The new value for the global maximum calculation step size. + * @throws {Error} - Throws an error if the value is less than or equal to 0. + */ +function setGlobalMaxCalcStepSize(value: number | Distance): void { + const convertedValue = unitTypeCoerce(value, Distance, preferredUnits.distance); + if (convertedValue.rawValue <= 0) { + throw new Error("_globalMaxCalcStepSize has to be > 0"); + } + _globalMaxCalcStepSize = convertedValue; +} + +/** + * Sets the global setting for powder sensitivity usage. + * @param {boolean} value - The new setting for powder sensitivity. + * @throws {TypeError} - Throws a TypeError if the value is not a boolean. + */ +function setGlobalUsePowderSensitivity(value: boolean): void { + if (typeof value !== "boolean") { + throw new TypeError(`setGlobalUsePowderSensitivity ${value} is not a boolean`); + } + _globalUsePowderSensitivity = value; +} + +/** + * Represents a point in a curve with three coefficients. + */ +class CurvePoint { + constructor( + public a: number, + public b: number, + public c: number + ) { + } +} + +/** + * Represents an array of `CurvePoint` instances. + */ +type Curve = CurvePoint[] + +/** + * Defines the properties required for a trajectory calculation. + */ +interface TrajectoryInterface { + lookAngle: number, + twist: number, + length: number, + diameter: number, + weight: number, + barrelElevation: number, + barrelAzimuth: number, + sightHeight: number, + cantCosine: number, + cantSine: number, + alt0: number, + calcStep: number, + muzzleVelocity: number + stabilityCoefficient: number +} + + +class TrajectoryCalc { + + readonly ammo: Ammo; + + protected _bc: number; + protected _tableData: DragTable; + protected _curve: Curve; + protected _gravityVector: Vector; + protected _t_props: TrajectoryInterface; + + /** + * Creates an instance of `TrajectoryCalc`. + * @param {Ammo} ammo - The ammunition instance containing drag model and other data. + */ + constructor(ammo: Ammo) { + this.ammo = ammo; + this._bc = ammo.dm.bc; + this._tableData = this.ammo.dm.dragTable; + this._curve = calculateCurve(this._tableData); + this._gravityVector = new Vector(.0, cGravityConstant, .0) + } + + /** + * Retrieves the drag table data used in trajectory calculations. + * @returns {DragTable} The drag table data. + */ + get tableData(): DragTable { + return this._tableData + } + + /** + * Retrieves the calculation step size for trajectory calculations. + * @param {number} [step=0] - The step size to retrieve. + * @returns {number} The calculation step size. + */ + public static getCalcStep(step: number = 0): number { + const preferredStep = _globalMaxCalcStepSize.In(Distance.Foot); + + if (step === 0) { + return preferredStep / 2.0; + } + return Math.min(step, preferredStep) / 2.0; + } + + /** + * Calculates the trajectory of a shot based on the given parameters. + * @param {Shot} shotInfo - The shot information including weapon, ammo, and angles. + * @param {Distance} maxRange - The maximum range to calculate the trajectory for. + * @param {Distance} distStep - The step size for distance intervals in the trajectory calculation. + * @param {boolean} [extraData=false] - Flag indicating whether to include additional data in the trajectory results. + * @returns {TrajectoryData[]} - An array of trajectory data points. + */ + public trajectory(shotInfo: Shot, maxRange: Distance, distStep: Distance, extraData: boolean = false): TrajectoryData[] { + let _distStep: Distance = unitTypeCoerce(distStep, Distance, preferredUnits.distance); + let filterFlags = TrajFlag.RANGE + + if (extraData) { + _distStep = UNew.Foot(0.2) + filterFlags = TrajFlag.ALL + } + + this._initTrajectory(shotInfo) + + return this._trajectory(shotInfo, maxRange.In(Distance.Foot), _distStep.In(Distance.Foot), filterFlags); + } + + /** + * Calculates the angle needed to zero the weapon for a given distance. + * @param {Shot} shotInfo - The shot information including weapon, ammo, and angles. + * @param {Distance} distance - The distance at which to zero the weapon. + * @returns {Angular} - The angle required to zero the weapon at the specified distance. + */ + public zeroAngle(shotInfo: Shot, distance: Distance): Angular { + this._initTrajectory(shotInfo) + + let zeroDistance = Math.cos(this._t_props.lookAngle) * distance.In(Distance.Foot) + let heightAtZero = Math.sin(this._t_props.lookAngle) * distance.In(Distance.Foot) + let maximumRange = zeroDistance - (1.5 * this._t_props.calcStep) + + let iterationsCount = 0 + let zeroFindingError = cZeroFindingAccuracy * 2 + let height: number + let t: TrajectoryData + + while (zeroFindingError > cZeroFindingAccuracy && iterationsCount < cMaxIterations) { + t = this._trajectory(shotInfo, maximumRange, zeroDistance, TrajFlag.NONE)[0] + height = t.height.In(Distance.Foot) + zeroFindingError = Math.abs(height - heightAtZero) + if (zeroFindingError > cZeroFindingAccuracy) { + this._t_props.barrelElevation -= (height - heightAtZero) / zeroDistance + } else { + break + } + iterationsCount += 1 + } + + if (zeroFindingError > cZeroFindingAccuracy) { + throw new Error(`Zero vertical error ${zeroFindingError} feet, after ${iterationsCount} iterations.`) + } + + return UNew.Radian(this._t_props.barrelElevation) + } + + /** + * Calculates the trajectory data for a shot over a range of distances. + * @param {Shot} shotInfo - The shot information including weapon, ammo, and angles. + * @param {number} maxRange - The maximum range for the trajectory calculation. + * @param {number} distStep - The step size for distance increments in the calculation. + * @param {TrajFlag} filterFlags - Flags to filter the trajectory data output. + * @returns {TrajectoryData[]} - An array of trajectory data points for the specified range and step size. + * @private + */ + _trajectory(shotInfo: Shot, maxRange: number, distStep: number, filterFlags: TrajFlag): TrajectoryData[] { + let ranges: TrajectoryData[] = []; + const rangesLength: number = Math.floor(maxRange / distStep) + 1; + let time: number = .0; + let previousMach: number = .0; + let drag: number = .0; + + let mach: number = .0 + let densityFactor: number = .0 + + const lenWinds: number = shotInfo.winds.length; + let currentWind: number = .0; + let currentItem: number = .0; + let nextRangeDistance: number = .0; + let nextWindRange: number = Wind.MAX_DISTANCE_FEET; + + let windVector: Vector + if (lenWinds < 1) { + windVector = new Vector(.0, .0, .0) + } else { + windVector = windToVector(shotInfo.winds[0]) + nextWindRange = shotInfo.winds[0].untilDistance.In(Distance.Foot) + } + + let velocity: number = this._t_props.muzzleVelocity + let rangeVector: Vector = new Vector( + .0, + -this._t_props.cantCosine * this._t_props.sightHeight, + -this._t_props.cantSine * this._t_props.sightHeight + ) + let velocityVector: Vector = new Vector( + Math.cos(this._t_props.barrelElevation) * Math.cos(this._t_props.barrelAzimuth), + Math.sin(this._t_props.barrelElevation), + Math.cos(this._t_props.barrelElevation) * Math.sin(this._t_props.barrelAzimuth) + ).mulByConst(velocity) + + let seenZero = TrajFlag.NONE + + if (rangeVector.y >= 0) { + seenZero |= TrajFlag.ZERO_UP; + } else if (rangeVector.y < 0 && this._t_props.barrelElevation < this._t_props.lookAngle) { + seenZero |= TrajFlag.ZERO_DOWN; + } + + let _flag = TrajFlag.NONE + + while (rangeVector.x <= maxRange + this._t_props.calcStep) { + _flag = TrajFlag.NONE + + if (rangeVector.x >= nextWindRange) { + currentWind += 1 + if (currentWind >= lenWinds) { + windVector = new Vector(.0, .0, .0) + nextWindRange = Wind.MAX_DISTANCE_FEET + } else { + windVector = windToVector(shotInfo.winds[currentWind]) + nextWindRange = shotInfo.winds[currentWind].untilDistance.In(Distance.Foot) + } + } + + [densityFactor, mach] = shotInfo.atmo.getDensityFactorAndMachForAltitude(this._t_props.alt0 + rangeVector.y) + + if (filterFlags) { + if (rangeVector.x > 0) { + let referenceHeight = rangeVector.x * Math.tan(this._t_props.lookAngle) + + if (!(seenZero & TrajFlag.ZERO_UP)) { + if (rangeVector.y >= referenceHeight) { + _flag |= TrajFlag.ZERO_UP + seenZero |= TrajFlag.ZERO_UP + } + } else if (!(seenZero & TrajFlag.ZERO_DOWN)) { + if (rangeVector.y < referenceHeight) { + _flag |= TrajFlag.ZERO_DOWN + seenZero |= TrajFlag.ZERO_DOWN + } + } + } + + if (previousMach > 1 && 1 >= velocity / mach) { + _flag |= TrajFlag.MACH + } + + if (rangeVector.x >= nextRangeDistance) { + _flag |= TrajFlag.RANGE + nextRangeDistance += distStep + currentItem += 1 + } + + if (_flag & filterFlags) { + ranges.push(createTrajectoryRow( + time, rangeVector, velocityVector, + velocity, mach, this.spinDrift(time), this._t_props.lookAngle, + densityFactor, drag, this._t_props.weight, _flag + )) + if (currentItem === rangesLength) { + break + } + } + } + + previousMach = velocity / mach + + let deltaTime: number = this._t_props.calcStep / velocityVector.x; + let velocityAdjusted: Vector = velocityVector.subtract(windVector) + velocity = velocityAdjusted.magnitude() + drag = densityFactor * velocity * this.dragByMach(velocity / mach) + velocityVector = velocityVector.subtract(velocityAdjusted.mulByConst(drag).subtract(this._gravityVector).mulByConst(deltaTime)) + let deltaRangeVector: Vector = new Vector( + this._t_props.calcStep, + velocityVector.y * deltaTime, + velocityVector.z * deltaTime + ) + rangeVector = rangeVector.add(deltaRangeVector) + velocity = velocityVector.magnitude() + time += deltaRangeVector.magnitude() / velocity + + if (velocity < cMinimumVelocity || rangeVector.y < cMaximumDrop) { + break + } + } + + if (!filterFlags) { + ranges.push(createTrajectoryRow( + time, rangeVector, velocityVector, + velocity, mach, this.spinDrift(time), this._t_props.lookAngle, + densityFactor, drag, this._t_props.weight, _flag + )) + } + + return ranges + } + + /** + * Initializes the trajectory properties based on the provided shot information. + * @param {Shot} shotInfo - The shot information including weapon, ammo, and environmental conditions. + * @private + */ + _initTrajectory(shotInfo: Shot): void { + this._t_props = { + lookAngle: shotInfo.lookAngle.In(Angular.Radian), + twist: shotInfo.weapon.twist.In(Distance.Inch), + length: shotInfo.ammo.dm.length.In(Distance.Inch), + diameter: shotInfo.ammo.dm.diameter.In(Distance.Inch), + weight: shotInfo.ammo.dm.weight.In(Weight.Grain), + barrelElevation: shotInfo.barrelElevation.In(Angular.Radian), + barrelAzimuth: shotInfo.barrelAzimuth.In(Angular.Radian), + sightHeight: shotInfo.weapon.sightHeight.In(Distance.Foot), + cantCosine: Math.cos(shotInfo.cantAngle.In(Angular.Radian)), + cantSine: Math.sin(shotInfo.cantAngle.In(Angular.Radian)), + alt0: shotInfo.atmo.altitude.In(Distance.Foot), + calcStep: TrajectoryCalc.getCalcStep(), + muzzleVelocity: (_globalUsePowderSensitivity ? + shotInfo.ammo.getVelocityForTemp(shotInfo.atmo.temperature) : + shotInfo.ammo.mv).In(Velocity.FPS), + stabilityCoefficient: 0 + } + this._t_props.stabilityCoefficient = this.calcStabilityCoefficient(shotInfo.atmo) + } + + /** + * Calculates the drag coefficient for a given Mach number. + * @param {number} mach - The Mach number for which the drag coefficient is to be calculated. + * @returns {number} - The calculated drag coefficient for the provided Mach number. + */ + public dragByMach(mach: number): number { + const cd = calculateByCurve(this._tableData, this._curve, mach); // Assuming `calculateByCurve` exists + return cd * 2.08551e-04 / this._bc; + } + + /** + * Calculates the spin drift of the projectile over time. + * @param {number} time - The time in seconds for which the spin drift is to be calculated. + * @returns {number} - The calculated spin drift in inches over the specified time. + */ + public spinDrift(time: number): number { + if (this._t_props.twist !== 0) { + const sign = this._t_props.twist > 0 ? 1 : -1; + return sign * (1.25 * (this._t_props.stabilityCoefficient + 1.2) * Math.pow(time, 1.83)) / 12; + } + return 0; + } + + /** + * Calculates the stability coefficient of the projectile based on atmospheric conditions. + * @param {Atmo} atmo - The atmospheric conditions including temperature, pressure, and humidity. + * @returns {number} - The calculated stability coefficient. + */ + public calcStabilityCoefficient(atmo: Atmo): number { + if (this._t_props.twist && this._t_props.length && this._t_props.diameter) { + const twistRate = Math.abs(this._t_props.twist) / this._t_props.diameter; + const lengthRatio = this._t_props.length / this._t_props.diameter; + + // Miller stability formula + const sd = 30 * this._t_props.weight / ( + Math.pow(twistRate, 2) * Math.pow(this._t_props.diameter, 3) * lengthRatio * (1 + Math.pow(lengthRatio, 2)) + ); + + // Velocity correction factor + const fv = Math.pow(this._t_props.muzzleVelocity / 2800, 1.0 / 3.0); + + // Atmospheric correction + const ft = atmo.temperature.In(Temperature.Fahrenheit); // Assuming a method to convert to Fahrenheit + const pt = atmo.pressure.In(Pressure.InHg); // Assuming a method to convert to InHg + const ftp = ((ft + 460) / (59 + 460)) * (29.92 / pt); + + return sd * fv * ftp; + } + return 0; + } + +} + +/** + * Converts wind data into a vector representation. + * @param {Wind} wind - The wind data including velocity and direction. + * @returns {Vector} - The vector representation of the wind. + */ +function windToVector(wind: Wind): Vector { + const rangeComponent = wind.velocity.In(Velocity.FPS) * Math.cos(wind.directionFrom.In(Angular.Radian)) + const crossComponent = wind.velocity.In(Velocity.FPS) * Math.sin(wind.directionFrom.In(Angular.Radian)) + return new Vector(rangeComponent, .0, crossComponent) +} + +/** + * Creates a trajectory data row for a given time step. + * @param {number} time - The time at which the trajectory data is calculated. + * @param {Vector} rangeVector - The vector representing the range. + * @param {Vector} velocityVector - The vector representing the velocity. + * @param {number} velocity - The magnitude of the velocity. + * @param {number} mach - The Mach number corresponding to the velocity. + * @param {number} spinDrift - The spin drift effect on the trajectory. + * @param {number} lookAngle - The angle of the sight relative to the bore axis. + * @param {number} densityFactor - The atmospheric density factor affecting the trajectory. + * @param {number} drag - The drag force experienced by the projectile. + * @param {number} weight - The weight of the projectile. + * @param {number} flag - Flags indicating specific trajectory conditions or calculations. + * @returns {TrajectoryData} - An object containing the calculated trajectory data. + */ +function createTrajectoryRow( + time: number, + rangeVector: Vector, + velocityVector: Vector, + velocity: number, + mach: number, + spinDrift: number, + lookAngle: number, + densityFactor: number, + drag: number, + weight: number, + flag: number +): TrajectoryData { + const windage = rangeVector.z + spinDrift + const dropAdjustment = getCorrection(rangeVector.x, rangeVector.y) + const windageAdjustment = getCorrection(rangeVector.x, windage) + const trajectoryAngle = Math.atan(velocityVector.y / velocityVector.x) + + return new TrajectoryData( + time, + UNew.Foot(rangeVector.x), + UNew.FPS(velocity), + velocity / mach, + UNew.Foot(rangeVector.y), + UNew.Foot((rangeVector.y - rangeVector.x * Math.tan(lookAngle)) * Math.cos(lookAngle)), + UNew.Radian(dropAdjustment - (rangeVector.x ? lookAngle : 0)), + UNew.Foot(windage), + UNew.Radian(windageAdjustment), + UNew.Foot(rangeVector.x / Math.cos(lookAngle)), + UNew.Radian(trajectoryAngle), + densityFactor - 1, + drag, + UNew.FootPound(calculateEnergy(weight, velocity)), + UNew.Pound(calculateOGW(weight, velocity)), + flag + ) +} + +/** + * Calculates the correction needed for a given distance and offset. + * @param {number} distance - The distance for which the correction is to be calculated. + * @param {number} offset - The offset that affects the correction calculation. + * @returns {number} - The calculated correction value. + */ +function getCorrection(distance: number, offset: number): number { + // Sight adjustment in radians + if (distance != 0) { + return Math.atan(offset / distance) + } + return 0 +} + +/** + * Calculates the kinetic energy of a bullet based on its weight and velocity. + * @param {number} bulletWeight - The weight of the bullet in grains. + * @param {number} velocity - The velocity of the bullet in feet per second (FPS). + * @returns {number} - The calculated kinetic energy in foot-pounds (ft-lb). + */ +function calculateEnergy(bulletWeight: number, velocity: number): number { + // energy in ft-lbs + return bulletWeight * Math.pow(velocity, 2) / 450400 +} + +/** + * Calculates the optical ground weight (OGW) of a bullet based on its weight and velocity. + * @param {number} bulletWeight - The weight of the bullet in grains. + * @param {number} velocity - The velocity of the bullet in feet per second (FPS). + * @returns {number} - The calculated optical ground weight. + */ +function calculateOGW(bulletWeight: number, velocity: number): number { + // Optimal Game Weight in pounds + return Math.pow(bulletWeight, 2) * Math.pow(velocity, 3) * 1.5e-12 +} + +/** + * Calculates a curve based on drag data points for trajectory analysis. + * @param {DragTable} dataPoints - An array of `DragDataPoint` objects representing the drag data. + * @returns {Curve} - An array of `CurvePoint` objects representing the calculated curve. + */ +function calculateCurve(dataPoints: DragTable): Curve { + let rate: number = + (dataPoints[1].CD - dataPoints[0].CD) / (dataPoints[1].Mach - dataPoints[0].Mach); + let curve: Curve = [ + new CurvePoint(0, rate, dataPoints[0].CD - dataPoints[0].Mach * rate), + ]; + const lenDataPoints: number = dataPoints.length; + const lenDataRange: number = lenDataPoints - 1; + + let x1, x2, x3, y1, y2, y3, a, b, c: number; + + for (let i: number = 1; i < lenDataRange; i++) { + x1 = dataPoints[i - 1].Mach; + x2 = dataPoints[i].Mach; + x3 = dataPoints[i + 1].Mach; + y1 = dataPoints[i - 1].CD; + y2 = dataPoints[i].CD; + y3 = dataPoints[i + 1].CD; + a = + ((y3 - y1) * (x2 - x1) - (y2 - y1) * (x3 - x1)) / + ((x3 * x3 - x1 * x1) * (x2 - x1) - (x2 * x2 - x1 * x1) * (x3 - x1)); + b = (y2 - y1 - a * (x2 * x2 - x1 * x1)) / (x2 - x1); + c = y1 - (a * x1 * x1 + b * x1); + curve.push(new CurvePoint(a, b, c)); + } + + let numPoints: number = lenDataPoints; + rate = + (dataPoints[numPoints - 1].CD - dataPoints[numPoints - 2].CD) / + (dataPoints[numPoints - 1].CD - dataPoints[numPoints - 2].Mach); + curve.push( + new CurvePoint( + 0, + rate, + dataPoints[numPoints - 1].CD - dataPoints[numPoints - 2].Mach * rate + ) + ); + return curve; +} + +/** + * Calculates a value based on drag data and a provided curve using a given Mach number. + * @param {DragTable} data - An array of `DragDataPoint` objects representing the drag data. + * @param {CurvePoint[]} curve - An array of `CurvePoint` objects representing the curve used for interpolation. + * @param {number} mach - The Mach number to use for the calculation. + * @returns {number} - The calculated value based on the curve and Mach number. + */ +function calculateByCurve(data: DragTable, curve: CurvePoint[], mach: number): number { + let m: number = 0; + let mid: number = 0; + let mlo: number = 0; + let mhi: number = curve.length - 2; + + while (mhi - mlo > 1) { + mid = Math.round((mhi + mlo) / 2.0); + if (data[mid].Mach < mach) { + mlo = mid; + } else { + mhi = mid; + } + + if (data[mhi].Mach - mach > mach - data[mlo].Mach) { + m = mlo; + } else { + m = mhi; + } + } + const curveM: CurvePoint = curve[m]; + return curveM.c + mach * (curveM.b + curveM.a * mach); +} + + +// Export the classes and constants +export default TrajectoryCalc; +export { + getGlobalMaxCalcStepSize, + getGlobalUsePowderSensitivity, + setGlobalMaxCalcStepSize, + setGlobalUsePowderSensitivity, + resetGlobals +} diff --git a/src/v2/trajectory_data.ts b/src/v2/trajectory_data.ts new file mode 100644 index 0000000..303ad62 --- /dev/null +++ b/src/v2/trajectory_data.ts @@ -0,0 +1,353 @@ +// Flags for marking trajectory row if Zero or Mach crossing +// Also uses to set a filters for a trajectory calculation loop +import { + UnitProps, + Unit, + preferredUnits, + unitTypeCoerce, + Angular, + Distance, + Velocity, + Energy, + Weight, + AbstractUnit +} from "./unit"; +import { Weapon } from "./munition"; +import { Shot } from "./conditions"; + +enum TrajFlag { + NONE = 0, + ZERO_UP = 1 << 0, + ZERO_DOWN = 1 << 1, + MACH = 1 << 2, + RANGE = 1 << 3, + DANGER = 1 << 4, + ZERO = ZERO_UP | ZERO_DOWN, + ALL = ZERO | MACH | RANGE | DANGER +} + +class TrajectoryData { + /** + * Represents data related to a trajectory calculation. + * This class is used solely as a return value from trajectory calculations. + * + * @class + * @param {number} time - The time elapsed in the trajectory calculation. + * @param {Distance} distance - The distance traveled. + * @param {Velocity} velocity - The velocity at the given point. + * @param {number} mach - The Mach number at the given point. + * @param {Distance} height - The height above the reference point. + * @param {Distance} targetDrop - The drop from the target elevation. + * @param {Angular} dropAdjustment - Adjustment in angle due to drop. + * @param {Distance} windage - The amount of windage correction. + * @param {Angular} windageAdjustment - Adjustment in angle due to windage. + * @param {Distance} lookDistance - The distance to the target. + * @param {Angular} angle - The angle of the trajectory. + * @param {number} densityFactor - Factor representing air density effects. + * @param {number} drag - The drag experienced by the projectile. + * @param {Energy} energy - The energy of the projectile at the given point. + * @param {Weight} ogw - The optimal gun weight. + * @param {TrajFlag} flag - Flags representing various trajectory characteristics. + */ + constructor( + readonly time: number, + readonly distance: Distance, + readonly velocity: Velocity, + readonly mach: number, + readonly height: Distance, + readonly targetDrop: Distance, + readonly dropAdjustment: Angular, + readonly windage: Distance, + readonly windageAdjustment: Angular, + readonly lookDistance: Distance, + readonly angle: Angular, + readonly densityFactor: number, + readonly drag: number, + readonly energy: Energy, + readonly ogw: Weight, + readonly flag: TrajFlag + ) { + this.time = time; + this.distance = distance; + this.velocity = velocity; + this.mach = mach; + this.height = height; + this.targetDrop = targetDrop; + this.dropAdjustment = dropAdjustment; + this.windage = windage; + this.windageAdjustment = windageAdjustment; + this.lookDistance = lookDistance; + this.angle = angle; + this.densityFactor = densityFactor; + this.drag = drag; + this.energy = energy; + this.ogw = ogw; + this.flag = flag; + } + + /** + * Returns an array of numerical values representing the trajectory data in default units. + * + * @returns {number[]} An array where each element corresponds to a specific piece of trajectory data + * converted to default units. + */ + inDefUnits(): number[] { + return [ + this.time, + this.distance.In(preferredUnits.distance), + this.velocity.In(preferredUnits.velocity), + this.mach, + this.height.In(preferredUnits.distance), + this.targetDrop.In(preferredUnits.drop), + this.dropAdjustment.In(preferredUnits.adjustment), + this.windage.In(preferredUnits.drop), + this.windageAdjustment.In(preferredUnits.adjustment), + this.lookDistance.In(preferredUnits.distance), + this.angle.In(preferredUnits.angular), + this.densityFactor, + this.drag, + this.energy.In(preferredUnits.energy), + this.ogw.In(preferredUnits.ogw), + this.flag + ] + } + + /** + * Returns an array of strings representing the trajectory data in a formatted manner. + * + * @returns {string[]} An array of formatted strings, each representing a piece of trajectory data. + */ + formatted(): string[] { + + /** simple formatter + * @param {AbstractUnit} value + * @param {Unit} unit + * @return {string} time + */ + function _fmt(value: AbstractUnit, unit: Unit): string { + return `${value.In(unit).toFixed(UnitProps[unit].accuracy)}${UnitProps[unit].symbol}`; + } + + return [ + `${this.time.toFixed(2)} s`, + _fmt(this.distance, preferredUnits.distance), + _fmt(this.velocity, preferredUnits.velocity), + `${this.mach.toFixed(2)} mach`, + _fmt(this.height, preferredUnits.distance), + _fmt(this.targetDrop, preferredUnits.drop), + _fmt(this.dropAdjustment, preferredUnits.adjustment), + _fmt(this.windage, preferredUnits.drop), + _fmt(this.windageAdjustment, preferredUnits.adjustment), + _fmt(this.lookDistance, preferredUnits.distance), + _fmt(this.angle, preferredUnits.angular), + `${this.densityFactor.toFixed(3)}`, + `${this.drag.toFixed(3)}`, + _fmt(this.energy, preferredUnits.energy), + _fmt(this.ogw, preferredUnits.ogw), + `${this.flag}` + ] + } +} + + +class DangerSpace { + + /** + * Stores the danger space data for a specified distance. + * ! DATACLASS, USES AS RETURNED VALUE ONLY + * + * @param {TrajectoryData} atRange - The trajectory data at the specified range. + * @param {number | Distance | null} targetHeight - The height of the target, or null if not applicable. + * @param {TrajectoryData} begin - The starting trajectory data for the danger space. + * @param {TrajectoryData} end - The ending trajectory data for the danger space. + * @param {number | Angular | null} lookAngle - The look angle for the danger space, or null if not applicable. + */ + constructor( + readonly atRange: TrajectoryData, + readonly targetHeight: Distance, + readonly begin: TrajectoryData, + readonly end: TrajectoryData, + readonly lookAngle: Angular + ) { + this.atRange = atRange; + this.targetHeight = targetHeight; + this.begin = begin; + this.end = end; + this.lookAngle = lookAngle; + } + + /** + * Returns a string representation of the DangerSpace object. + * @returns {string} - A string summarizing the DangerSpace data. + */ + toString(): string { + return `Danger space at ${this.atRange.distance.to(preferredUnits.distance)} ` + + `for ${this.targetHeight.to(preferredUnits.drop)} tall target ` + + `ranges from ${this.begin.distance.to(preferredUnits.distance)} ` + + `to ${this.end.distance.to(preferredUnits.distance)}`; + } +} + + +class HitResult { + /** Results of the shot + * ! DATACLASS, USES AS RETURNED VALUE ONLY + * @param {Shot} shot + * @param {TrajectoryData[]} _trajectory + * @param {boolean} _extra + */ + + readonly shot: Shot + readonly _trajectory: TrajectoryData[] + readonly _extra: boolean = false + + constructor( + shot: Shot, + trajectory: TrajectoryData[], + extra: boolean = false, + ) { + this.shot = shot + this._trajectory = trajectory + this._extra = extra + } + + /** + * Returns a copy of the trajectory data as an array. + * @returns {TrajectoryData[]} - A new array containing the trajectory data. + */ + toArray(): TrajectoryData[] { + return [...this._trajectory]; + } + + // get(index: number) { + // return this._trajectory[index]; + // } + + /** + * Gets the trajectory data. + * @returns {TrajectoryData[]} - The trajectory data array. + */ + get trajectory(): TrajectoryData[] { + return this._trajectory + } + + /** + * Gets the extra data flag. + * @returns {boolean} - True if extra data is included, otherwise false. + */ + get extra(): boolean { + return this._extra + } + + protected _checkExtra(): void { + if (!this._extra) { + throw new Error(`${Object.getPrototypeOf(this).constructor.name} has no extra data. Use Calculator.fire(..., extra_data=true)`); + } + } + + zeros(): TrajectoryData[] { + this._checkExtra(); + + const data = this._trajectory.filter(row => row.flag & TrajFlag.ZERO); + if (data.length < 1) { + throw new Error("Can't find zero crossing points"); + } + + return data; + } + + /** + * Finds the index of the TrajectoryData item closest to the given distance. + * @param {Distance} distance - The distance to search for. + * @returns {number} - The index of the closest TrajectoryData item. + */ + indexAtDistance(distance: Distance): number { + return this.trajectory.findIndex(item => item.distance.rawValue >= distance.rawValue); + } + + getAtDistance(d: Distance): TrajectoryData { + const index = this.indexAtDistance(d); + if (index < 0) { + throw new Error(`Calculated trajectory doesn't reach requested distance ${d}`); + } + return this.trajectory[index]; + } + + /** + * Calculates the danger space for the specified range and target height. + * @param {number | Distance} atRange - The distance at which to calculate the danger space. + * @param {number | Distance} targetHeight - The height of the target. + * @param {number | Angular | null} lookAngle - The look angle for the calculation. + * @returns {DangerSpace} - The computed DangerSpace object. + */ + public dangerSpace(atRange: (number | Distance), + targetHeight: (number | Distance), + lookAngle: (number | Angular | null) = null): DangerSpace { + this._checkExtra(); + + const _atRange: Distance = unitTypeCoerce(atRange, Distance, preferredUnits.distance); + + const _targetHeight: Distance = unitTypeCoerce(targetHeight, Distance, preferredUnits.distance); + const _targetHeightHalf: number = _targetHeight.rawValue / 2.0; + + const _lookAngle = lookAngle ? this.shot.lookAngle : unitTypeCoerce(lookAngle ?? 0, Angular, preferredUnits.angular) + + // Get index of first trajectory point with distance >= at_range + const index = this.indexAtDistance(_atRange) + if (index < 0) { + throw new Error( + `Calculated trajectory doesn't reach requested distance ${atRange}` + ) + } + + const findBeginDanger = (rowNum: number) => { + /** + * Beginning of danger space is last .distance' < .distance where + * (.drop' - target_center) >= target_height/2 + * @param {number} rowNum - Index of the trajectory point for which we are calculating danger space + * @return {TrajectoryData} - Distance marking the beginning of danger space + */ + const centerRow = this.trajectory[rowNum]; + + for (let i = rowNum - 1; i >= 0; i--) { + const primeRow = this.trajectory[i]; + if ((primeRow.targetDrop.rawValue - centerRow.targetDrop.rawValue) >= _targetHeightHalf) { + return primeRow; + } + } + + return this.trajectory[0]; + }; + + const findEndDanger = (rowNum: number) => { + /** + * End of danger space is first .distance' > .distance where + * (target_center - .drop') >= target_height/2 + * @param {number} rowNum - Index of the trajectory point for which we are calculating danger space + * @return {TrajectoryData} - Distance marking the end of danger space + */ + const centerRow = this.trajectory[rowNum]; + + for (let i = rowNum + 1; i < this.trajectory.length; i++) { + const primeRow = this.trajectory[i]; + if ((centerRow.targetDrop.rawValue - primeRow.targetDrop.rawValue) >= _targetHeightHalf) { + return primeRow; + } + } + + return this.trajectory[this.trajectory.length - 1]; + }; + + return new DangerSpace( + this._trajectory[index], + _targetHeight, + findBeginDanger(index), + findEndDanger(index), + _lookAngle + ); + } + +} + + +export { TrajectoryData, TrajFlag, DangerSpace, HitResult } \ No newline at end of file diff --git a/src/v2/unit.ts b/src/v2/unit.ts new file mode 100644 index 0000000..bd9b574 --- /dev/null +++ b/src/v2/unit.ts @@ -0,0 +1,842 @@ +// Use-full types for units of measurement conversion for ballistics calculations + + +// Unit types enum +enum Unit { + Radian = 0, + Degree = 1, + MOA = 2, + MIL = 3, + MRad = 4, + Thousand = 5, + InchesPer100Yd = 6, + CmPer100M = 7, + OClock = 8, + Inch = 10, + Foot = 11, + Yard = 12, + Mile = 13, + NauticalMile = 14, + Millimeter = 15, + Centimeter = 16, + Meter = 17, + Kilometer = 18, + Line = 19, + FootPound = 30, + Joule = 31, + MmHg = 40, + InHg = 41, + Bar = 42, + hPa = 43, + PSI = 44, + Fahrenheit = 50, + Celsius = 51, + Kelvin = 52, + Rankin = 53, + MPS = 60, + KMH = 61, + FPS = 62, + MPH = 63, + KT = 64, + Grain = 70, + Ounce = 71, + Gram = 72, + Pound = 73, + Kilogram = 74, + Newton = 75, +} + + +class AbstractUnit { + /** + * Abstract class for unit of measure instance definition. + * Stores defined unit and value, applies conversions to other units. + * + * @param {number} value - Numeric value of the unit. + * @param {Unit} units - Unit as Unit enum. + */ + + ["constructor"]: typeof AbstractUnit; + _value: number + _definedUnits: Unit + + constructor(value: number, units: Unit) { + // this["constructor"] = this.constructor; + this._value = this.toRaw(value, units); + this._definedUnits = units; + } + + /** + * Returns a human-readable representation of the value with its unit. + * + * @return {string} A string representing the value with its unit. + */ + toString(): string { + // Extract the unit details based on the defined units. + const units = this._definedUnits; + const props = UnitProps[units]; + + // Convert the raw value to the specified unit. + const v = this.fromRaw(this._value, units); + + // Format the value with a fixed number of decimal places and concatenate the unit symbol. + return `${v.toFixed(props.accuracy)}${props.symbol}`; + } + + /** + * Validates the units. + * + * @param {number} value - Value of the unit. + * @param {Unit} units - Unit enum type. + * @return {number} Value in specified units. + * @throws {TypeError} When the provided units are not of the expected type. + * @throws {Error} When the provided units are not supported. + */ + protected _unit_support_error(value: number, units: any): number { + + if (!(units instanceof this.constructor)) { + const err_msg = `Type expected: ${this.constructor.name}, ${typeof units} found: ${units} (${value})`; + throw new TypeError(err_msg); + } + + if (!Object.values(this).includes(units)) { + throw new Error(`${this.constructor.name}: unit ${units} is not supported`); + } + + return 0; + } + + /** + * Converts value with specified units to raw value. + * + * @param {number} value - Value of the unit. + * @param {Unit} units - Unit enum type. + * @return {number} Value in specified units. + */ + protected toRaw(value: number, units: Unit): number { + return this._unit_support_error(value, units); + } + + /** + * Converts raw value to specified units. + * + * @param {number} value - Raw value of the unit. + * @param {Unit} units - Unit enum type. + * @return {number} Value in specified units. + */ + protected fromRaw(value: number, units: Unit): number { + return this._unit_support_error(value, units); + } + + /** + * Returns a new unit instance in specified units. + * + * @param {Unit} units - Unit enum type. + * @return {AbstractUnit} New unit instance in specified units. + */ + to(units: Unit): AbstractUnit { + const value: number = this.In(units); + return new this.constructor(value, units); + } + + /** + * Returns value in specified units. + * + * @param {Unit} units - Unit enum type. + * @return {number} Value in specified units. + */ + In(units: Unit): number { + return this.fromRaw(this._value, units); + } + + /** + * Returns defined units. + * + * @return {Unit} Defined units. + */ + get units(): Unit { + return this._definedUnits; + } + + /** + * Raw unit value getter. + * + * @return {number} Raw unit value. + */ + get rawValue(): number { + return this._value; + } +} + +/** + * Angular unit + */ +class Angular extends AbstractUnit { + + // Angular unit constants + static Radian = Unit.Radian; + static Degree = Unit.Degree; + static MOA = Unit.MOA; + static MIL = Unit.MIL; + static MRad = Unit.MRad; + static Thousand = Unit.Thousand; + static InchesPer100Yd = Unit.InchesPer100Yd; + static CmPer100M = Unit.CmPer100M; + static OClock = Unit.OClock; + + constructor(value: number, units: Unit) { + super(value, units); + } + + protected toRaw(value: number, units: Unit): number { + if (units === Unit.Radian) { + return value; + } + if (units === Unit.Degree) { + return (value / 180) * Math.PI; + } + if (units === Unit.MOA) { + return (value / 180) * Math.PI / 60; + } + if (units === Unit.MIL) { + return (value / 3200) * Math.PI; + } + if (units === Unit.MRad) { + return value / 1000; + } + if (units === Unit.Thousand) { + return (value / 3000) * Math.PI; + } + if (units === Unit.InchesPer100Yd) { + return Math.atan(value / 3600); + } + if (units === Unit.CmPer100M) { + return Math.atan(value / 10000); + } + if (units === Unit.OClock) { + return (value / 6) * Math.PI; + } + + return super.toRaw(value, units); + } + + protected fromRaw(value: number, units: Unit): number { + if (units === Unit.Radian) { + return value; + } + if (units === Unit.Degree) { + return (value * 180) / Math.PI; + } + if (units === Unit.MOA) { + return (value * 180) / Math.PI * 60; + } + if (units === Unit.MIL) { + return (value * 3200) / Math.PI; + } + if (units === Unit.MRad) { + return value * 1000; + } + if (units === Unit.Thousand) { + return (value * 3000) / Math.PI; + } + if (units === Unit.InchesPer100Yd) { + return Math.tan(value) * 3600; + } + if (units === Unit.CmPer100M) { + return Math.tan(value) * 10000; + } + if (units === Unit.OClock) { + return (value * 6) / Math.PI; + } + + return super.fromRaw(value, units); + } +} + +/** + * Distance unit + */ +class Distance extends AbstractUnit { + + // Distance unit constants + static Inch = Unit.Inch; + static Foot = Unit.Foot; + static Yard = Unit.Yard; + static Mile = Unit.Mile; + static NauticalMile = Unit.NauticalMile; + static Line = Unit.Line; + static Millimeter = Unit.Millimeter; + static Centimeter = Unit.Centimeter; + static Meter = Unit.Meter; + static Kilometer = Unit.Kilometer; + + constructor(value: number, units: Unit) { + super(value, units); + } + + protected toRaw(value: number, units: Unit): number { + if (units === Unit.Inch) { + return value; + } + if (units === Unit.Foot) { + return value * 12; + } else if (units === Unit.Yard) { + return value * 36; + } else if (units === Unit.Mile) { + return value * 63360; + } else if (units === Unit.NauticalMile) { + return value * 72913.3858; + } else if (units === Unit.Line) { + return value / 10; + } else if (units === Unit.Millimeter) { + return value / 25.4; + } else if (units === Unit.Centimeter) { + return value / 2.54; + } else if (units === Unit.Meter) { + return value / 25.4 * 1000; + } else if (units === Unit.Kilometer) { + return value / 25.4 * 1000000; + } else { + return super.toRaw(value, units); + } + } + + protected fromRaw(value: number, units: Unit): number { + if (units === Unit.Inch) { + return value; + } + if (units === Unit.Foot) { + return value / 12; + } else if (units === Unit.Yard) { + return value / 36; + } else if (units === Unit.Mile) { + return value / 63360; + } else if (units === Unit.NauticalMile) { + return value / 72913.3858; + } else if (units === Unit.Line) { + return value * 10; + } else if (units === Unit.Millimeter) { + return value * 25.4; + } else if (units === Unit.Centimeter) { + return value * 2.54; + } else if (units === Unit.Meter) { + return value * 25.4 / 1000; + } else if (units === Unit.Kilometer) { + return value * 25.4 / 1000000; + } else { + return super.fromRaw(value, units); + } + } +} + +/** + * Velocity unit + */ +class Velocity extends AbstractUnit { + + // Velocity unit constants + static MPS = Unit.MPS; + static KMH = Unit.KMH; + static FPS = Unit.FPS; + static MPH = Unit.MPH; + static KT = Unit.KT; + + constructor(value: number, units: Unit) { + super(value, units); + } + + protected toRaw(value: number, units: Unit): number { + if (units === Unit.MPS) { + return value; + } + if (units === Unit.KMH) { + return value / 3.6; + } + if (units === Unit.FPS) { + return value / 3.2808399; + } + if (units === Unit.MPH) { + return value / 2.23693629; + } + if (units === Unit.KT) { + return value / 1.94384449; + } + return super.toRaw(value, units); + } + + protected fromRaw(value: number, units: Unit): number { + if (units === Unit.MPS) { + return value; + } + if (units === Unit.KMH) { + return value * 3.6; + } + if (units === Unit.FPS) { + return value * 3.2808399; + } + if (units === Unit.MPH) { + return value * 2.23693629; + } + if (units === Unit.KT) { + return value * 1.94384449; + } + return super.fromRaw(value, units); + } +} + +/** + * Weight unit + */ +class Weight extends AbstractUnit { + + // Weight unit constants + static Grain = Unit.Grain; + static Ounce = Unit.Ounce; + static Gram = Unit.Gram; + static Pound = Unit.Pound; + static Kilogram = Unit.Kilogram; + static Newton = Unit.Newton; + + constructor(value: number, units: Unit) { + super(value, units); + } + + protected toRaw(value: number, units: Unit): number { + if (units === Unit.Grain) { + return value; + } + if (units === Unit.Gram) { + return value * 15.4323584; + } + if (units === Unit.Kilogram) { + return value * 15432.3584; + } + if (units === Unit.Newton) { + return value * 151339.73750336; + } + if (units === Unit.Pound) { + return value / 0.000142857143; + } + if (units === Unit.Ounce) { + return value * 437.5; + } + return super.toRaw(value, units); + } + + protected fromRaw(value: number, units: Unit): number { + if (units === Unit.Grain) { + return value; + } + if (units === Unit.Gram) { + return value / 15.4323584; + } + if (units === Unit.Kilogram) { + return value / 15432.3584; + } + if (units === Unit.Newton) { + return value / 151339.73750336; + } + if (units === Unit.Pound) { + return value * 0.000142857143; + } + if (units === Unit.Ounce) { + return value / 437.5; + } + return super.fromRaw(value, units); + } +} + +/** + * Pressure unit + */ +class Pressure extends AbstractUnit { + + // Pressure unit constants + static MmHg = Unit.MmHg; + static InHg = Unit.InHg; + static Bar = Unit.Bar; + static hPa = Unit.hPa; + static PSI = Unit.PSI; + + constructor(value: number, units: Unit) { + super(value, units); + } + + protected toRaw(value: number, units: Unit): number { + if (units === Unit.MmHg) { + return value; + } + if (units === Unit.InHg) { + return value * 25.4; + } + if (units === Unit.Bar) { + return value * 750.061683; + } + if (units === Unit.hPa) { + return value * 750.061683 / 1000; + } + if (units === Unit.PSI) { + return value * 51.714924102396; + } + return super.toRaw(value, units); + } + + protected fromRaw(value: number, units: Unit): number { + if (units === Unit.MmHg) { + return value; + } + if (units === Unit.InHg) { + return value / 25.4; + } + if (units === Unit.Bar) { + return value / 750.061683; + } + if (units === Unit.hPa) { + return value / 750.061683 * 1000; + } + if (units === Unit.PSI) { + return value / 51.714924102396; + } + return super.fromRaw(value, units); + } +} + +/** + * Temperature unit + */ +class Temperature extends AbstractUnit { + + // Temperature unit constants + static Fahrenheit = Unit.Fahrenheit; + static Celsius = Unit.Celsius; + static Kelvin = Unit.Kelvin; + static Rankin = Unit.Rankin; + + constructor(value: number, units: Unit) { + super(value, units); + } + + protected toRaw(value: number, units: Unit): number { + if (units === Unit.Fahrenheit) { + return value; + } + if (units === Unit.Rankin) { + return value - 459.67; + } + if (units === Unit.Celsius) { + return value * 9 / 5 + 32; + } + if (units === Unit.Kelvin) { + return (value - 273.15) * 9 / 5 + 32; + } + return super.toRaw(value, units); + } + + protected fromRaw(value: number, units: Unit): number { + if (units === Unit.Fahrenheit) { + return value; + } + if (units === Unit.Rankin) { + return value + 459.67; + } + if (units === Unit.Celsius) { + return (value - 32) * 5 / 9; + } + if (units === Unit.Kelvin) { + return (value - 32) * 5 / 9 + 273.15; + } + return super.fromRaw(value, units); + } +} + +/** + * Energy unit + */ +class Energy extends AbstractUnit { + + // Energy unit constants + static FootPound = Unit.FootPound; + static Joule = Unit.Joule; + + constructor(value: number, units: Unit) { + super(value, units); + } + + protected toRaw(value: number, units: Unit): number { + if (units === Unit.FootPound) { + return value; + } + if (units === Unit.Joule) { + return value * 0.737562149277; + } + return super.toRaw(value, units); + } + + protected fromRaw(value: number, units: Unit): number { + if (units === Unit.FootPound) { + return value; + } + if (units === Unit.Joule) { + return value / 0.737562149277; + } + return super.fromRaw(value, units); + } +} + + +// Dict of properties of the Unit enum type +const UnitProps = { + [Unit.Radian]: { name: 'radian', accuracy: 6, symbol: 'rad' }, + [Unit.Degree]: { name: 'degree', accuracy: 4, symbol: '°' }, + [Unit.MOA]: { name: 'MOA', accuracy: 2, symbol: 'MOA' }, + [Unit.MIL]: { name: 'MIL', accuracy: 2, symbol: 'MIL' }, + [Unit.MRad]: { name: 'MRAD', accuracy: 2, symbol: 'MRAD' }, + [Unit.Thousand]: { name: 'thousand', accuracy: 2, symbol: 'ths' }, + [Unit.InchesPer100Yd]: { name: 'inches/100yd', accuracy: 2, symbol: 'in/100yd' }, + [Unit.CmPer100M]: { name: 'cm/100m', accuracy: 2, symbol: 'cm/100m' }, + [Unit.OClock]: { name: 'hour', accuracy: 2, symbol: 'h' }, + + [Unit.Inch]: { name: 'inch', accuracy: 3, symbol: 'inch' }, + [Unit.Foot]: { name: 'foot', accuracy: 2, symbol: 'ft' }, + [Unit.Yard]: { name: 'yard', accuracy: 3, symbol: 'yd' }, + [Unit.Mile]: { name: 'mile', accuracy: 3, symbol: 'mi' }, + [Unit.NauticalMile]: { name: 'nautical mile', accuracy: 3, symbol: 'nm' }, + [Unit.Millimeter]: { name: 'millimeter', accuracy: 3, symbol: 'mm' }, + [Unit.Centimeter]: { name: 'centimeter', accuracy: 3, symbol: 'cm' }, + [Unit.Meter]: { name: 'meter', accuracy: 3, symbol: 'm' }, + [Unit.Kilometer]: { name: 'kilometer', accuracy: 3, symbol: 'km' }, + [Unit.Line]: { name: 'line', accuracy: 3, symbol: 'ln' }, + + [Unit.FootPound]: { name: 'foot * pound', accuracy: 0, symbol: 'ft·lb' }, + [Unit.Joule]: { name: 'joule', accuracy: 0, symbol: 'J' }, + + [Unit.MmHg]: { name: 'mmHg', accuracy: 0, symbol: 'mmHg' }, + [Unit.InHg]: { name: 'inHg', accuracy: 6, symbol: 'inHg' }, + [Unit.Bar]: { name: 'bar', accuracy: 2, symbol: 'bar' }, + [Unit.hPa]: { name: 'hPa', accuracy: 4, symbol: 'hPa' }, + [Unit.PSI]: { name: 'psi', accuracy: 4, symbol: 'psi' }, + + [Unit.Fahrenheit]: { name: 'fahrenheit', accuracy: 1, symbol: '°F' }, + [Unit.Celsius]: { name: 'celsius', accuracy: 1, symbol: '°C' }, + [Unit.Kelvin]: { name: 'kelvin', accuracy: 1, symbol: '°K' }, + [Unit.Rankin]: { name: 'rankin', accuracy: 1, symbol: '°R' }, + + [Unit.MPS]: { name: 'mps', accuracy: 0, symbol: 'm/s' }, + [Unit.KMH]: { name: 'kmh', accuracy: 1, symbol: 'km/h' }, + [Unit.FPS]: { name: 'fps', accuracy: 1, symbol: 'ft/s' }, + [Unit.MPH]: { name: 'mph', accuracy: 1, symbol: 'mph' }, + [Unit.KT]: { name: 'knots', accuracy: 1, symbol: 'kt' }, + + [Unit.Grain]: { name: 'grain', accuracy: 1, symbol: 'gr' }, + [Unit.Ounce]: { name: 'ounce', accuracy: 1, symbol: 'oz' }, + [Unit.Gram]: { name: 'gram', accuracy: 1, symbol: 'g' }, + [Unit.Pound]: { name: 'pound', accuracy: 3, symbol: 'lb' }, + [Unit.Kilogram]: { name: 'kilogram', accuracy: 3, symbol: 'kg' }, + [Unit.Newton]: { name: 'newton', accuracy: 3, symbol: 'N' }, +}; + + +const Measure = { + Angular: Angular, + Distance: Distance, + Velocity: Velocity, + Weight: Weight, + Temperature: Temperature, + Pressure: Pressure, + Energy: Energy, +} + + +const UNew = { + Radian: (value: number) => new Angular(value, Unit.Radian), + Degree: (value: number) => new Angular(value, Unit.Degree), + MOA: (value: number) => new Angular(value, Unit.MOA), + MIL: (value: number) => new Angular(value, Unit.MIL), + MRad: (value: number) => new Angular(value, Unit.MRad), + Thousand: (value: number) => new Angular(value, Unit.Thousand), + InchesPer100Yd: (value: number) => new Angular(value, Unit.InchesPer100Yd), + CmPer100M: (value: number) => new Angular(value, Unit.CmPer100M), + OClock: (value: number) => new Angular(value, Unit.OClock), + Inch: (value: number) => new Distance(value, Unit.Inch), + Foot: (value: number) => new Distance(value, Unit.Foot), + Yard: (value: number) => new Distance(value, Unit.Yard), + Mile: (value: number) => new Distance(value, Unit.Mile), + NauticalMile: (value: number) => new Distance(value, Unit.NauticalMile), + Millimeter: (value: number) => new Distance(value, Unit.Millimeter), + Centimeter: (value: number) => new Distance(value, Unit.Centimeter), + Meter: (value: number) => new Distance(value, Unit.Meter), + Kilometer: (value: number) => new Distance(value, Unit.Kilometer), + Line: (value: number) => new Distance(value, Unit.Line), + FootPound: (value: number) => new Energy(value, Unit.FootPound), + Joule: (value: number) => new Energy(value, Unit.Joule), + MmHg: (value: number) => new Pressure(value, Unit.MmHg), + InHg: (value: number) => new Pressure(value, Unit.InHg), + Bar: (value: number) => new Pressure(value, Unit.Bar), + hPa: (value: number) => new Pressure(value, Unit.hPa), + PSI: (value: number) => new Pressure(value, Unit.PSI), + Fahrenheit: (value: number) => new Temperature(value, Unit.Fahrenheit), + Celsius: (value: number) => new Temperature(value, Unit.Celsius), + Kelvin: (value: number) => new Temperature(value, Unit.Kelvin), + Rankin: (value: number) => new Temperature(value, Unit.Rankin), + MPS: (value: number) => new Velocity(value, Unit.MPS), + KMH: (value: number) => new Velocity(value, Unit.KMH), + FPS: (value: number) => new Velocity(value, Unit.FPS), + MPH: (value: number) => new Velocity(value, Unit.MPH), + KT: (value: number) => new Velocity(value, Unit.KT), + Grain: (value: number) => new Weight(value, Unit.Grain), + Ounce: (value: number) => new Weight(value, Unit.Ounce), + Gram: (value: number) => new Weight(value, Unit.Gram), + Pound: (value: number) => new Weight(value, Unit.Pound), + Kilogram: (value: number) => new Weight(value, Unit.Kilogram), + Newton: (value: number) => new Weight(value, Unit.Newton), + + [Unit.Radian]: (value: number) => new Angular(value, Unit.Radian), + [Unit.Degree]: (value: number) => new Angular(value, Unit.Degree), + [Unit.MOA]: (value: number) => new Angular(value, Unit.MOA), + [Unit.MIL]: (value: number) => new Angular(value, Unit.MIL), + [Unit.MRad]: (value: number) => new Angular(value, Unit.MRad), + [Unit.Thousand]: (value: number) => new Angular(value, Unit.Thousand), + [Unit.InchesPer100Yd]: (value: number) => new Angular(value, Unit.InchesPer100Yd), + [Unit.CmPer100M]: (value: number) => new Angular(value, Unit.CmPer100M), + [Unit.OClock]: (value: number) => new Angular(value, Unit.OClock), + [Unit.Inch]: (value: number) => new Distance(value, Unit.Inch), + [Unit.Foot]: (value: number) => new Distance(value, Unit.Foot), + [Unit.Yard]: (value: number) => new Distance(value, Unit.Yard), + [Unit.Mile]: (value: number) => new Distance(value, Unit.Mile), + [Unit.NauticalMile]: (value: number) => new Distance(value, Unit.NauticalMile), + [Unit.Millimeter]: (value: number) => new Distance(value, Unit.Millimeter), + [Unit.Centimeter]: (value: number) => new Distance(value, Unit.Centimeter), + [Unit.Meter]: (value: number) => new Distance(value, Unit.Meter), + [Unit.Kilometer]: (value: number) => new Distance(value, Unit.Kilometer), + [Unit.Line]: (value: number) => new Distance(value, Unit.Line), + [Unit.FootPound]: (value: number) => new Energy(value, Unit.FootPound), + [Unit.Joule]: (value: number) => new Energy(value, Unit.Joule), + [Unit.MmHg]: (value: number) => new Pressure(value, Unit.MmHg), + [Unit.InHg]: (value: number) => new Pressure(value, Unit.InHg), + [Unit.Bar]: (value: number) => new Pressure(value, Unit.Bar), + [Unit.hPa]: (value: number) => new Pressure(value, Unit.hPa), + [Unit.PSI]: (value: number) => new Pressure(value, Unit.PSI), + [Unit.Fahrenheit]: (value: number) => new Temperature(value, Unit.Fahrenheit), + [Unit.Celsius]: (value: number) => new Temperature(value, Unit.Celsius), + [Unit.Kelvin]: (value: number) => new Temperature(value, Unit.Kelvin), + [Unit.Rankin]: (value: number) => new Temperature(value, Unit.Rankin), + [Unit.MPS]: (value: number) => new Velocity(value, Unit.MPS), + [Unit.KMH]: (value: number) => new Velocity(value, Unit.KMH), + [Unit.FPS]: (value: number) => new Velocity(value, Unit.FPS), + [Unit.MPH]: (value: number) => new Velocity(value, Unit.MPH), + [Unit.KT]: (value: number) => new Velocity(value, Unit.KT), + [Unit.Grain]: (value: number) => new Weight(value, Unit.Grain), + [Unit.Ounce]: (value: number) => new Weight(value, Unit.Ounce), + [Unit.Gram]: (value: number) => new Weight(value, Unit.Gram), + [Unit.Pound]: (value: number) => new Weight(value, Unit.Pound), + [Unit.Kilogram]: (value: number) => new Weight(value, Unit.Kilogram), + [Unit.Newton]: (value: number) => new Weight(value, Unit.Newton) +}; + + +/** + * Coerces the given instance to the specified class type or creates a new instance. + * + * @param {AbstractUnit|Object} instance - The instance to coerce or create. + * @param {AbstractUnit|Object|function} expectedClass - The expected class type. + * @param {Unit|number} defaultUnit - The default unit for creating a new instance. + * @returns {AbstractUnit} An instance of the expected class type. + * @throws {TypeError} If the instance is not of the expected class type or 'number'. + */ +function unitTypeCoerce( + instance: (number | AbstractUnit), + expectedClass: (typeof AbstractUnit | any), + defaultUnit: Unit +): any { + if (instance instanceof expectedClass) { + // If the instance is already of the expected class type, return it. + return instance; + } else if (typeof instance === 'number') { + // If the instance is a number, create a new instance using the default unit. + return new expectedClass(instance, defaultUnit); + } + else { + // If the instance is not of the expected type, throw a TypeError. + throw new TypeError(`Instance must be a type of ${expectedClass.name + } or 'number'`); + } +} + + +export interface IPreferredUnits { + angular: Unit; + distance: Unit; + velocity: Unit; + pressure: Unit; + temperature: Unit; + diameter: Unit; + length: Unit; + weight: Unit; + adjustment: Unit; + drop: Unit; + energy: Unit; + ogw: Unit; + sight_height: Unit; + target_height: Unit; + twist: Unit; + defaults(): void; + setUnits(units: Partial): void; +} + +// Type guard to check if a value is a valid Unit +function isUnit(value: any): value is Unit { + return Object.values(Unit).includes(value); +} + +export class PreferredUnits implements IPreferredUnits { + angular: Unit = Unit.Degree; + distance: Unit = Unit.Yard; + velocity: Unit = Unit.FPS; + pressure: Unit = Unit.InHg; + temperature: Unit = Unit.Fahrenheit; + diameter: Unit = Unit.Inch; + length: Unit = Unit.Inch; + weight: Unit = Unit.Grain; + adjustment: Unit = Unit.MIL; + drop: Unit = Unit.Inch; + energy: Unit = Unit.FootPound; + ogw: Unit = Unit.Pound; + sight_height: Unit = Unit.Inch; + target_height: Unit = Unit.Inch; + twist: Unit = Unit.Inch; + + defaults(): void { + this.angular = Unit.Degree; + this.distance = Unit.Yard; + this.velocity = Unit.FPS; + this.pressure = Unit.InHg; + this.temperature = Unit.Fahrenheit; + this.diameter = Unit.Inch; + this.length = Unit.Inch; + this.weight = Unit.Grain; + this.adjustment = Unit.MIL; + this.drop = Unit.Inch; + this.energy = Unit.FootPound; + this.ogw = Unit.Pound; + this.sight_height = Unit.Inch; + this.target_height = Unit.Inch; + this.twist = Unit.Inch; + } + + setUnits(units: Partial): void { + for (const [key, value] of Object.entries(units)) { + if (isUnit(value)) { + (this as any)[key] = value; + } else { + console.warn(`${value} is not a valid Unit`); + } + } + } +} + +const preferredUnits = new PreferredUnits(); + +export { + AbstractUnit, Angular, Distance, Velocity, Weight, Temperature, Pressure, Energy, + Unit, UnitProps, unitTypeCoerce, UNew, Measure, preferredUnits +} diff --git a/src/v2/vector.ts b/src/v2/vector.ts new file mode 100644 index 0000000..a1b9a2e --- /dev/null +++ b/src/v2/vector.ts @@ -0,0 +1,46 @@ +export default class Vector { + + constructor( + public x: number, + public y: number, + public z: number, + ) { } + + copy() { + return new Vector(this.x, this.y, this.z) + } + + magnitude(): number { + return Math.sqrt( + Math.pow(this.x, 2) + Math.pow(this.y, 2) + Math.pow(this.z, 2) + ); + } + + mulByConst(a: number): Vector { + return new Vector(this.x * a, this.y * a, this.z * a); + } + + mulByVector(b: Vector): number { + return this.x * b.x + this.y * b.y + this.z * b.z; + } + + add(b: Vector): Vector { + return new Vector(this.x + b.x, this.y + b.y, this.z + b.z); + } + + subtract(b: Vector): Vector { + return new Vector(this.x - b.x, this.y - b.y, this.z - b.z); + } + + negate(): Vector { + return new Vector(-this.x, -this.y, -this.z); + } + + normalize(): Vector { + const m: number = this.magnitude(); + if (Math.abs(m) < 1e-10) { + return new Vector(this.x, this.y, this.z); + } + return this.mulByConst(1.0 / m); + } +} \ No newline at end of file