Skip to content
Draft
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
95 changes: 95 additions & 0 deletions src/src/app/dive-planner-service/BuhlmannZHL16C.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { BuhlmannZHL16C } from './BuhlmannZHL16C';
import { BreathingGas } from './BreathingGas';
import { DiveSettingsService } from './DiveSettings.service';
import { DiveSegment } from './DiveSegment';

describe('BuhlmannZHL16C', () => {
let diveSettings: DiveSettingsService;
let air: BreathingGas;
let model: BuhlmannZHL16C;

beforeEach(() => {
diveSettings = new DiveSettingsService();
// Initialize StandardGases to avoid the find error
BreathingGas.StandardGases = [];
air = BreathingGas.create(21, 0, 79, diveSettings);
model = new BuhlmannZHL16C();
});

it('should calculate time to fly for surface conditions', () => {
// At surface with no dive, should be 0 time to fly
const timeToFly = model.getTimeToFly();
expect(timeToFly).toBe(0);
});

it('should calculate time to fly after a dive', () => {
// Simulate a dive to 30m for 30 minutes
const segment = new DiveSegment(0, 1800, 'Test', 'Test dive', 30, 30, air, 'test', diveSettings);
model.calculateForSegment(segment);

Check failure on line 29 in src/src/app/dive-planner-service/BuhlmannZHL16C.spec.ts

View workflow job for this annotation

GitHub Actions / build

Delete `····`

Check failure on line 29 in src/src/app/dive-planner-service/BuhlmannZHL16C.spec.ts

View workflow job for this annotation

GitHub Actions / build

Delete `····`
// Should have some time to fly after nitrogen loading
const timeToFly = model.getTimeToFly();
expect(timeToFly).toBeDefined();
expect(timeToFly).toBeGreaterThan(0);

Check failure on line 34 in src/src/app/dive-planner-service/BuhlmannZHL16C.spec.ts

View workflow job for this annotation

GitHub Actions / build

Delete `····`

Check failure on line 34 in src/src/app/dive-planner-service/BuhlmannZHL16C.spec.ts

View workflow job for this annotation

GitHub Actions / build

Delete `····`
// Should be reasonable time (less than 24 hours for a moderate dive)
expect(timeToFly).toBeLessThan(24 * 3600);
});

it('should calculate longer time to fly for deeper/longer dives', () => {
// Simulate two different dives
const shallowDive = new DiveSegment(0, 1800, 'Shallow', 'Shallow dive', 20, 20, air, 'test', diveSettings);
const deepDive = new DiveSegment(0, 1800, 'Deep', 'Deep dive', 40, 40, air, 'test', diveSettings);

Check failure on line 43 in src/src/app/dive-planner-service/BuhlmannZHL16C.spec.ts

View workflow job for this annotation

GitHub Actions / build

Delete `····`

Check failure on line 43 in src/src/app/dive-planner-service/BuhlmannZHL16C.spec.ts

View workflow job for this annotation

GitHub Actions / build

Delete `····`
const shallowModel = new BuhlmannZHL16C();
const deepModel = new BuhlmannZHL16C();

Check failure on line 46 in src/src/app/dive-planner-service/BuhlmannZHL16C.spec.ts

View workflow job for this annotation

GitHub Actions / build

Delete `····`

Check failure on line 46 in src/src/app/dive-planner-service/BuhlmannZHL16C.spec.ts

View workflow job for this annotation

GitHub Actions / build

Delete `····`
shallowModel.calculateForSegment(shallowDive);
deepModel.calculateForSegment(deepDive);

Check failure on line 49 in src/src/app/dive-planner-service/BuhlmannZHL16C.spec.ts

View workflow job for this annotation

GitHub Actions / build

Delete `····`

Check failure on line 49 in src/src/app/dive-planner-service/BuhlmannZHL16C.spec.ts

View workflow job for this annotation

GitHub Actions / build

Delete `····`
const shallowTimeToFly = shallowModel.getTimeToFly();
const deepTimeToFly = deepModel.getTimeToFly();

Check failure on line 52 in src/src/app/dive-planner-service/BuhlmannZHL16C.spec.ts

View workflow job for this annotation

GitHub Actions / build

Delete `····`

Check failure on line 52 in src/src/app/dive-planner-service/BuhlmannZHL16C.spec.ts

View workflow job for this annotation

GitHub Actions / build

Delete `····`
expect(shallowTimeToFly).toBeDefined();
expect(deepTimeToFly).toBeDefined();

Check failure on line 55 in src/src/app/dive-planner-service/BuhlmannZHL16C.spec.ts

View workflow job for this annotation

GitHub Actions / build

Delete `····`

Check failure on line 55 in src/src/app/dive-planner-service/BuhlmannZHL16C.spec.ts

View workflow job for this annotation

GitHub Actions / build

Delete `····`
if (shallowTimeToFly !== undefined && deepTimeToFly !== undefined) {
expect(deepTimeToFly).toBeGreaterThan(shallowTimeToFly);
}
});

it('should use custom flying threshold', () => {
// Simulate a dive to 30m for 30 minutes
const segment = new DiveSegment(0, 1800, 'Test', 'Test dive', 30, 30, air, 'test', diveSettings);
model.calculateForSegment(segment);

Check failure on line 65 in src/src/app/dive-planner-service/BuhlmannZHL16C.spec.ts

View workflow job for this annotation

GitHub Actions / build

Delete `····`

Check failure on line 65 in src/src/app/dive-planner-service/BuhlmannZHL16C.spec.ts

View workflow job for this annotation

GitHub Actions / build

Delete `····`
// Compare different thresholds
const conservativeTime = model.getTimeToFly(0.80); // More conservative

Check failure on line 67 in src/src/app/dive-planner-service/BuhlmannZHL16C.spec.ts

View workflow job for this annotation

GitHub Actions / build

Delete `0`

Check failure on line 67 in src/src/app/dive-planner-service/BuhlmannZHL16C.spec.ts

View workflow job for this annotation

GitHub Actions / build

Delete `0`
const standardTime = model.getTimeToFly(0.869); // Standard (110% of ambient)
const liberalTime = model.getTimeToFly(0.95); // More liberal

Check failure on line 70 in src/src/app/dive-planner-service/BuhlmannZHL16C.spec.ts

View workflow job for this annotation

GitHub Actions / build

Delete `····`

Check failure on line 70 in src/src/app/dive-planner-service/BuhlmannZHL16C.spec.ts

View workflow job for this annotation

GitHub Actions / build

Delete `····`
expect(conservativeTime).toBeDefined();
expect(standardTime).toBeDefined();
expect(liberalTime).toBeDefined();

if (conservativeTime !== undefined && standardTime !== undefined && liberalTime !== undefined) {
expect(conservativeTime).toBeGreaterThan(standardTime);
expect(standardTime).toBeGreaterThan(liberalTime);
}
});

it('should return controlling tissue time', () => {
// Simulate a dive that loads multiple tissues
const segment = new DiveSegment(0, 3600, 'Test', 'Long dive', 25, 25, air, 'test', diveSettings);
model.calculateForSegment(segment);

const timeToFly = model.getTimeToFly();

// The result should be reasonable for a long shallow dive
expect(timeToFly).toBeDefined();
expect(timeToFly).toBeGreaterThan(0);

// Should be controlled by slower tissues for longer dives
expect(timeToFly).toBeGreaterThan(3600); // More than 1 hour for a 1-hour dive
});
});
9 changes: 9 additions & 0 deletions src/src/app/dive-planner-service/BuhlmannZHL16C.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,13 @@ export class BuhlmannZHL16C {
getTissuePHe(time: number, tissue: number): number {
return this.tissues[tissue - 1].getPHe(time);
}

getTimeToFly(flyingN2Threshold: number = 0.869): number | undefined {
const tissueTimes = this.tissues.map(t => t.getTimeToFly(flyingN2Threshold));
const validTimes = tissueTimes.filter(x => x !== undefined) as number[];

if (validTimes.length === 0) return undefined;

return Math.max(...validTimes);
}
}
15 changes: 15 additions & 0 deletions src/src/app/dive-planner-service/DivePlannerService.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ describe('DivePlannerService', () => {
expect(svc.getHypoxicError().duration).toBe(0);
expect(svc.getMaxDepth()).toBe(0);
expect(svc.getPO2Error().duration).toBe(0);

// Test time to fly at surface - should be 0
const timeToFly = svc.getTimeToFly();
expect(timeToFly).toBe(0);
});

it('30m for 25 mins on nitrox 32', () => {
Expand Down Expand Up @@ -78,6 +82,12 @@ describe('DivePlannerService', () => {
expect(svc.getMaxDepth()).toBe(30);
expect(svc.getPO2Error().duration).toBe(0);

// Test time to fly - should be reasonable for a recreational dive
const timeToFly = svc.getTimeToFly();
expect(timeToFly).toBeDefined();
expect(timeToFly).toBeGreaterThan(0);
expect(timeToFly).toBeLessThan(24 * 3600); // Less than 24 hours

expect(svc.getNoDecoLimit(25, air, 0)).toBe(518);
expect(svc.getOptimalDecoGas(25).oxygen).toBe(45);
expect(svc.getOptimalDecoGas(25).helium).toBe(0);
Expand Down Expand Up @@ -138,6 +148,11 @@ describe('DivePlannerService', () => {
expect(svc.getHypoxicError().duration).toBe(6);
expect(svc.getMaxDepth()).toBe(100);
expect(svc.getPO2Error().duration).toBe(708);

// Test time to fly - should be significant after a deep technical dive
const timeToFly = svc.getTimeToFly();
expect(timeToFly).toBeDefined();
expect(timeToFly).toBeGreaterThan(0);
});

it('NDL accounts for on-gassing during ascent', () => {
Expand Down
4 changes: 4 additions & 0 deletions src/src/app/dive-planner-service/DivePlannerService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,10 @@ export class DivePlannerService {
return this.diveProfile.getENDError();
}

getTimeToFly(flyingN2Threshold: number = 0.869): number | undefined {
return this.diveProfile.getTimeToFly(flyingN2Threshold);
}

getPO2WarningMessage(pO2: number): string | undefined {
if (pO2 > this.settings.workingPO2Maximum && pO2 <= this.settings.decoPO2Maximum)
return `Oxygen partial pressure should only go above ${this.settings.workingPO2Maximum} during deco stops`;
Expand Down
4 changes: 4 additions & 0 deletions src/src/app/dive-planner-service/DiveProfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,10 @@ export class DiveProfile {
return { end: maxEND, duration };
}

getTimeToFly(flyingN2Threshold: number = 0.869): number | undefined {
return this.algo.getTimeToFly(flyingN2Threshold);
}

getMaxDepth(): number {
return Math.max(...this.segments.map(x => x.EndDepth));
}
Expand Down
25 changes: 25 additions & 0 deletions src/src/app/dive-planner-service/Tissue.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Tissue } from './Tissue';
import { BreathingGas } from './BreathingGas';
import { DiveSettingsService } from './DiveSettings.service';
import { DiveSegment } from './DiveSegment';

describe('Tissue Time to Fly', () => {
let diveSettings: DiveSettingsService;
let air: BreathingGas;

beforeEach(() => {
diveSettings = new DiveSettingsService();
// Initialize StandardGases to avoid the find error
BreathingGas.StandardGases = [];
air = BreathingGas.create(21, 0, 79, diveSettings);
});

it('should return 0 for surface conditions', () => {
// Test tissue compartment 1 (fastest) - 5 minute N2 half-life
const tissue = new Tissue(1, 5, 1.1696, 0.5578, 1.51, 1.7474, 0.4245);

// At surface, should be 0 time to fly
const timeToFly = tissue.getTimeToFly();
expect(timeToFly).toBe(0);
});
});
27 changes: 27 additions & 0 deletions src/src/app/dive-planner-service/Tissue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,33 @@ export class Tissue {
return Math.floor(time);
}

getTimeToFly(flyingN2Threshold: number = 0.869): number | undefined {
const currentTime = this.tissueByTime.size - 1;
const currentPN2 = this.getPN2(currentTime);
const currentPHe = this.getPHe(currentTime);

// If we're already at safe levels, no waiting time needed
if (currentPN2 <= flyingN2Threshold && currentPHe <= this.ENVIRONMENT_PHE + 0.001) {
return 0;
}

// Surface gas pressures
const surfacePN2 = this.ENVIRONMENT_PN2;
const surfacePHe = this.ENVIRONMENT_PHE;

// Simple iterative approach - check every minute
for (let time = 1; time <= this.MAX_NDL; time++) {
const newPN2 = currentPN2 + this.getPN2DeltaByTime(currentPN2, surfacePN2, time);
const newPHe = currentPHe + this.getPHeDeltaByTime(currentPHe, surfacePHe, time);

if (newPN2 <= flyingN2Threshold && newPHe <= this.ENVIRONMENT_PHE + 0.001) {
return time;
}
}

return undefined; // If it takes longer than MAX_NDL
}

getPN2(time: number): number {
return this.getTissueByTime(time).PN2;
}
Expand Down
4 changes: 4 additions & 0 deletions src/src/app/dive-summary/dive-summary.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,8 @@
Average Depth:
<strong>{{ divePlanner.getAverageDepth() | number: '1.0-0' }}m</strong>
</div>
<div class="dive-stat">
Time to Fly:
<strong>{{ getTimeToFlyFormatted() }}</strong>
</div>
</div>
58 changes: 58 additions & 0 deletions src/src/app/dive-summary/dive-summary.component.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DiveSummaryComponent } from './dive-summary.component';
import { DivePlannerService } from '../dive-planner-service/DivePlannerService';
import { HumanDurationPipe } from '../pipes/human-duration.pipe';

describe('DiveSummaryComponent', () => {
let component: DiveSummaryComponent;
let fixture: ComponentFixture<DiveSummaryComponent>;
let mockDivePlannerService: jasmine.SpyObj<DivePlannerService>;

beforeEach(async () => {
mockDivePlannerService = jasmine.createSpyObj('DivePlannerService', ['getTimeToFly', 'getDiveDuration', 'getMaxDepth', 'getAverageDepth']);

await TestBed.configureTestingModule({
declarations: [DiveSummaryComponent, HumanDurationPipe],
providers: [
{ provide: DivePlannerService, useValue: mockDivePlannerService }
]
}).compileComponents();

fixture = TestBed.createComponent(DiveSummaryComponent);
component = fixture.componentInstance;

// Set up default mock returns
mockDivePlannerService.getDiveDuration.and.returnValue(1800);
mockDivePlannerService.getMaxDepth.and.returnValue(30);
mockDivePlannerService.getAverageDepth.and.returnValue(25);
});

it('should create', () => {
expect(component).toBeTruthy();
});

it('should format time to fly correctly for no waiting time', () => {
mockDivePlannerService.getTimeToFly.and.returnValue(0);
expect(component.getTimeToFlyFormatted()).toBe('None');
});

it('should format time to fly correctly for minutes only', () => {
mockDivePlannerService.getTimeToFly.and.returnValue(1800); // 30 minutes
expect(component.getTimeToFlyFormatted()).toBe('30 min');
});

it('should format time to fly correctly for hours only', () => {
mockDivePlannerService.getTimeToFly.and.returnValue(7200); // 2 hours
expect(component.getTimeToFlyFormatted()).toBe('2 hr');
});

it('should format time to fly correctly for hours and minutes', () => {
mockDivePlannerService.getTimeToFly.and.returnValue(9000); // 2 hours 30 minutes
expect(component.getTimeToFlyFormatted()).toBe('2 hr 30 min');
});

it('should format time to fly correctly for undefined (> 5 hours)', () => {
mockDivePlannerService.getTimeToFly.and.returnValue(undefined);
expect(component.getTimeToFlyFormatted()).toBe('> 5 hours');
});
});
23 changes: 23 additions & 0 deletions src/src/app/dive-summary/dive-summary.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,27 @@ import { DivePlannerService } from '../dive-planner-service/DivePlannerService';
})
export class DiveSummaryComponent {
constructor(public divePlanner: DivePlannerService) {}

getTimeToFlyFormatted(): string {
const timeToFly = this.divePlanner.getTimeToFly();

if (timeToFly === undefined) {
return '> 5 hours';
}

if (timeToFly === 0) {
return 'None';
}

const hours = Math.floor(timeToFly / 3600);
const minutes = Math.floor((timeToFly % 3600) / 60);

if (hours === 0) {
return `${minutes} min`;
} else if (minutes === 0) {
return `${hours} hr`;
} else {
return `${hours} hr ${minutes} min`;
}
}
}
Loading