Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions lib/src/trajectory_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,82 @@ class TrajectoryData {
return energy;
}
}

extension TrajectoryDataInterpolation on List<TrajectoryData> {
TrajectoryData interpolate(Distance travelDistance) {
for (final (a, b) in pairwise(this)) {
final da = a.travelDistance.value;
final db = b.travelDistance.value;
final dt = travelDistance.value;
final inRange = da <= dt && db >= dt;
if (!inRange) continue;

t(double a, double b) => lerp(a, b, da, db, dt);

return TrajectoryData(
time: Timespan(t(a.time.time, b.time.time)),
travelDistance: Distance(
t(a.travelDistance.value, b.travelDistance.value),
a.travelDistance.unit,
convert: false,
),
velocity: Velocity(
t(a.velocity.value, b.velocity.value),
a.velocity.unit,
convert: false,
),
mach: t(a.mach, b.mach),
drop: Distance(
t(a.drop.value, b.drop.value),
a.drop.unit,
convert: false,
),
dropAdjustment: Angular(
t(a.dropAdjustment.value, b.dropAdjustment.value),
a.dropAdjustment.unit,
convert: false,
),
windage: Distance(
t(a.windage.value, b.windage.value),
a.windage.unit,
convert: false,
),
windageAdjustment: Angular(
t(a.windageAdjustment.value, b.windageAdjustment.value),
a.windageAdjustment.unit,
convert: false,
),
energy: Energy(
t(a.energy.value, b.energy.value),
a.energy.unit,
convert: false,
),
optimalGameWeight: Weight(
t(a.optimalGameWeight.value, b.optimalGameWeight.value),
a.optimalGameWeight.unit,
convert: false,
),
);
}

throw RangeError('reference point is out of bounds');
}

// visible for testing
static List<(T, T)> pairwise<T>(List<T> source) {
return [
for (var i = 0; i < source.length - 1; i++)
(
source[i],
source[i + 1],
)
];
}

// visible for testing
static double lerp(double y1, double y2, double x1, double x2, double x) {
if (x < x1) throw RangeError("x must be greater than x1");
if (x > x2) throw RangeError("x must be less than x2");
return y1 + (y2 - y1) / (x2 - x1) * (x - x1);
}
}
94 changes: 94 additions & 0 deletions test/interpolation_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import 'package:ballistic/src/bmath/unit/unit.dart';
import 'package:ballistic/src/trajectory_data.dart';
import 'package:test/test.dart';

main() {
test('pairwise', () {
final pairwise = TrajectoryDataInterpolation.pairwise;
expect(pairwise([1, 2, 3]), [(1, 2), (2, 3)]);
});

test('lerp', () {
final lerp = TrajectoryDataInterpolation.lerp;

// simple sweep
expect(() => lerp(0, 1, 0, 1, -1), throwsRangeError);
expect(lerp(0, 1, 0, 1, 0), 0);
expect(lerp(0, 1, 0, 1, 0.5), 0.5);
expect(lerp(0, 1, 0, 1, 1), 1);
expect(() => lerp(0, 1, 0, 1, 2), throwsRangeError);

// complexish
expect(lerp(0, 50, 0, 200, 100), 25);
});

group('TrajectoryData interpolation', () {
final data = [
TrajectoryData(
drop: Distance(0, DistanceUnit.inch),
dropAdjustment: Angular(0, AngularUnit.moa),
energy: Energy(100, EnergyUnit.footPound),
mach: 0,
optimalGameWeight: Weight(100, WeightUnit.pound),
time: Timespan(0),
travelDistance: Distance(0, DistanceUnit.yard),
velocity: Velocity(500, VelocityUnit.fps),
windage: Distance(0, DistanceUnit.inch),
windageAdjustment: Angular(0, AngularUnit.moa),
),
TrajectoryData(
drop: Distance(1, DistanceUnit.inch),
dropAdjustment: Angular(1, AngularUnit.moa),
energy: Energy(80, EnergyUnit.footPound),
mach: 1,
optimalGameWeight: Weight(80, WeightUnit.pound),
time: Timespan(1),
travelDistance: Distance(100, DistanceUnit.yard),
velocity: Velocity(400, VelocityUnit.fps),
windage: Distance(1, DistanceUnit.inch),
windageAdjustment: Angular(1, AngularUnit.moa),
),
TrajectoryData(
drop: Distance(2, DistanceUnit.inch),
dropAdjustment: Angular(2, AngularUnit.moa),
energy: Energy(60, EnergyUnit.footPound),
mach: 2,
optimalGameWeight: Weight(60, WeightUnit.pound),
time: Timespan(2),
travelDistance: Distance(200, DistanceUnit.yard),
velocity: Velocity(300, VelocityUnit.fps),
windage: Distance(2, DistanceUnit.inch),
windageAdjustment: Angular(2, AngularUnit.moa),
),
];

test('in bounds, all fields', () {
final x = data.interpolate(Distance(50, DistanceUnit.yard));
expect(x.drop.unitValue, 0.5);
expect(x.dropAdjustment.unitValue, 0.5);
expect(x.energy.unitValue, 90);
expect(x.mach, 0.5);
expect(x.optimalGameWeight.unitValue, closeTo(90, .001));
expect(x.time.time, 0.5);
expect(x.travelDistance.unitValue, 50);
expect(x.velocity.unitValue, closeTo(450, .001));
expect(x.windage.unitValue, 0.5);
expect(x.windageAdjustment.unitValue, 0.5);
});

test('out of bounds', () {
expect(() => data.interpolate(Distance(5000, DistanceUnit.yard)),
throwsRangeError);
expect(() => data.interpolate(Distance(50, DistanceUnit.kilometer)),
throwsRangeError);
});

test('unit sweep', () {
fn(DistanceUnit d) => data.interpolate(Distance(50, d)).drop.unitValue;
expect(fn(DistanceUnit.centimeter), closeTo(0.0055, 0.001));
expect(fn(DistanceUnit.inch), closeTo(0.0138, 0.001));
expect(fn(DistanceUnit.yard), closeTo(0.5, 0.001));
expect(fn(DistanceUnit.meter), closeTo(0.547, 0.001));
});
});
}