diff --git a/docs/superpowers/plans/2026-07-05-dive-planner-phase5-contingencies.md b/docs/superpowers/plans/2026-07-05-dive-planner-phase5-contingencies.md new file mode 100644 index 000000000..2c16ff6cb --- /dev/null +++ b/docs/superpowers/plans/2026-07-05-dive-planner-phase5-contingencies.md @@ -0,0 +1,64 @@ +# Dive Planner Phase 5: Contingencies — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** One-tap deviation plans (+depth / +time / both) with ghost overlays on the canvas, lost-deco-gas schedules, turn-pressure per bottom tank, and rock-bottom (min-gas) validation — the spec's "Contingencies (Phase 5)". + +**Architecture:** A `ContingencyService` derives modified `DivePlan`s (deeper/longer bottom, tank removed) and runs them through the existing `PlanEngine`; nothing new in the engine core. Turn pressure and min-gas land as fields on `PlanTankUsage` plus one new `PlanIssueType.minGasViolation`. Editing state gains the contingency config (deltas, turn rule) like Phase 4's setpoints. Branch: stacked on `worktree-dive-planner-phase4-ccr` (PR #488). Global constraints as Phases 3–4. + +--- + +### Task 1: `ContingencyService` — deviations + lost gas + +**Files:** Create `lib/features/planner/domain/services/contingency_service.dart`; Test `test/features/planner/contingency_service_test.dart`. + +```dart +class DeviationOutcome { final String key; // 'deeper' | 'longer' | 'both' + final domain.DivePlan plan; final PlanOutcome outcome; } +class LostGasOutcome { final DiveTank tank; final PlanOutcome outcome; } +class ContingencyService { + const ContingencyService({PlanEngineConfig config}); + List deviations(domain.DivePlan plan); + List lostGas(domain.DivePlan plan); // OC only; [] for CCR +} +``` +- Deviations (skip when no segments): `deeper` = every bottom segment's depth + `deviationDepthDelta` (descent segments' end depth likewise when they feed the deepened bottom — implement as: any segment start/end depth that EQUALS the plan max depth gets +delta); `longer` = every bottom segment + `deviationTimeMinutes`; `both` = both edits. Each computed via `PlanEngine.compute`. +- Lost gas: for each `deco`/`stage`-role tank, a plan without it (and without segments referencing it — bottom segments never do) → outcome. Empty list for CCR (bailout covers loop loss) and when no such tanks. + +- [ ] Tests: deeper deviation has `maxDepth == base + delta` and `totalDecoSeconds > base`; longer likewise; lost EAN50 on a trimix plan lengthens deco vs base and its stops at ≤ 22 m no longer breathe EAN50; CCR plan → `lostGas` empty; deltas honored from the plan fields. +- [ ] Implement; format+commit `feat(planner): contingency service for deviation and lost-gas plans`. + +--- + +### Task 2: Turn pressure + min-gas in PlanEngine + +**Files:** Modify `plan_outcome.dart` (PlanTankUsage +`turnPressureBar`, +`minGasBar`; PlanIssueType +`minGasViolation`), `plan_engine.dart`, `plan_results_sheet.dart` (message case); ARBs (+`plannerCanvas_issue_minGas`, `plannerCanvas_gas_turnAt`, `plannerCanvas_gas_minGas`); Test extend `plan_engine_issues_test.dart`. + +- `PlanEngineConfig` += `buddyFactor = 2.0` (two divers share the rock-bottom ascent). +- Turn pressure (OC, rule non-null, bottom/backGas-role tanks with startPressure): `usable = start − reserve`, `turn = start − usable × fraction` where fraction = allUsable 1.0 / halves 0.5 / thirds 1/3 / custom `turnPressureFraction ?? 1/3`. +- Min gas (OC, backGas-role tanks): liters for a direct stressed ascent from `plan.maxDepth` on that tank's gas — schedule from maxDepth with `FixedAscentGas(tank gas)` at END-OF-BOTTOM tissue state is overkill; per rock-bottom convention use the NO-DECO emergency form: 1 min at max depth + ascent at `plan.ascentRate` to surface, at `sacStressedEffective × buddyFactor`, pressure-integrated — then `minGasBar` via the tank's volume (ideal-bar conversion is the field convention: liters / volume). Issue `minGasViolation` (alert) when `remainingPressure < minGasBar`. +- [ ] Tests: thirds rule on 200 bar start / 50 reserve → turn at 150; minGas positive and violation fires for a small tank on a deep plan, absent for a big one; CCR plans get neither. +- [ ] Format+commit `feat(planner): turn pressure and rock-bottom validation`. + +--- + +### Task 3: Editing state + mapper contingency config + +**Files:** `plan_result.dart` (DivePlanState += `deviationDepthDelta` (double, default 5), `deviationTimeMinutes` (int, default 5), `turnPressureRule` (TurnPressureRule?), `turnPressureFraction` (double?)), notifier `updateContingencies({...})`, mapper both ways; extend mapper test. +- [ ] Round-trip tests; format+commit `feat(planner): contingency config in the planner editing state`. + +--- + +### Task 4: Canvas UI — ghost overlays, contingency sections, settings + +**Files:** `plan_canvas_providers.dart` (extract `buildCanvasSeries({segments, outcome})` top-level; `planDeviationsProvider`, `selectedDeviationProvider = StateProvider`, `deviationGhostSeriesProvider`), `plan_canvas_chart.dart` (ghost `LineChartBarData`, grey dashed), new `contingency_chips.dart` (selector row: Base / +Xm / +X′ / both), `plan_results_sheet.dart` (Deviations + Lost gas sections with mini runtime tables; turn/min-gas on gas rows), new `contingency_settings_section.dart` (two numeric fields + rule dropdown, shown in plan settings for OC), page wiring; l10n (~8 keys, all locales); widget tests. +- [ ] Tests: selecting a deviation adds a second (ghost) line bar; deviations section lists three tables for an OC deco plan; gas row shows `turn @` when a rule is set. +- [ ] Format+commit `feat(planner): contingency overlays and tables on the canvas`. + +--- + +### Task 5: Verification sweep + +- [ ] `flutter analyze` clean; format stable; `flutter test test/core/deco/ test/features/planner/ test/features/dive_planner/` green; gen-l10n zero untranslated. + +**Out of scope:** slate export of contingency tables (Phase 7); per-stop lost-gas re-switching UI; cave same-way-back. diff --git a/lib/features/dive_planner/domain/entities/plan_result.dart b/lib/features/dive_planner/domain/entities/plan_result.dart index 4d2b2b2a1..1b4cf6aee 100644 --- a/lib/features/dive_planner/domain/entities/plan_result.dart +++ b/lib/features/dive_planner/domain/entities/plan_result.dart @@ -4,7 +4,7 @@ import 'package:submersion/core/deco/entities/tissue_compartment.dart'; import 'package:submersion/features/dive_log/domain/entities/dive.dart'; import 'package:submersion/features/dive_planner/domain/entities/plan_segment.dart'; import 'package:submersion/features/planner/domain/entities/dive_plan.dart' - show PlanMode; + show PlanMode, TurnPressureRule; /// Types of warnings that can occur during dive planning. enum PlanWarningType { @@ -509,6 +509,14 @@ class DivePlanState extends Equatable { /// Depth below which the high setpoint is in force; null = default 10 m. final double? setpointSwitchDepth; + /// Contingency deviation deltas (Phase 5). + final double deviationDepthDelta; + final int deviationTimeMinutes; + + /// Turn-pressure rule for penetration planning; null = none. + final TurnPressureRule? turnPressureRule; + final double? turnPressureFraction; + /// Reserve pressure in bar. final double reservePressure; @@ -540,6 +548,10 @@ class DivePlanState extends Equatable { this.setpointLow, this.setpointHigh, this.setpointSwitchDepth, + this.deviationDepthDelta = 5.0, + this.deviationTimeMinutes = 5, + this.turnPressureRule, + this.turnPressureFraction, this.reservePressure = kDefaultReservePressureBar, this.notes = '', this.isDirty = false, @@ -591,6 +603,11 @@ class DivePlanState extends Equatable { double? setpointLow, double? setpointHigh, double? setpointSwitchDepth, + double? deviationDepthDelta, + int? deviationTimeMinutes, + TurnPressureRule? turnPressureRule, + double? turnPressureFraction, + bool clearTurnPressureRule = false, double? reservePressure, String? notes, bool? isDirty, @@ -624,6 +641,14 @@ class DivePlanState extends Equatable { setpointSwitchDepth: clearSetpoints ? null : (setpointSwitchDepth ?? this.setpointSwitchDepth), + deviationDepthDelta: deviationDepthDelta ?? this.deviationDepthDelta, + deviationTimeMinutes: deviationTimeMinutes ?? this.deviationTimeMinutes, + turnPressureRule: clearTurnPressureRule + ? null + : (turnPressureRule ?? this.turnPressureRule), + turnPressureFraction: clearTurnPressureRule + ? null + : (turnPressureFraction ?? this.turnPressureFraction), reservePressure: reservePressure ?? this.reservePressure, notes: notes ?? this.notes, isDirty: isDirty ?? this.isDirty, @@ -649,6 +674,10 @@ class DivePlanState extends Equatable { setpointLow, setpointHigh, setpointSwitchDepth, + deviationDepthDelta, + deviationTimeMinutes, + turnPressureRule, + turnPressureFraction, reservePressure, notes, isDirty, diff --git a/lib/features/dive_planner/presentation/providers/dive_planner_providers.dart b/lib/features/dive_planner/presentation/providers/dive_planner_providers.dart index b5b3b211e..8893a8022 100644 --- a/lib/features/dive_planner/presentation/providers/dive_planner_providers.dart +++ b/lib/features/dive_planner/presentation/providers/dive_planner_providers.dart @@ -361,6 +361,25 @@ class DivePlanNotifier extends StateNotifier { ); } + /// Update contingency configuration; only supplied values change. + void updateContingencies({ + double? depthDelta, + int? timeMinutes, + domain.TurnPressureRule? turnRule, + double? turnFraction, + bool clearTurnRule = false, + }) { + state = state.copyWith( + deviationDepthDelta: depthDelta, + deviationTimeMinutes: timeMinutes, + turnPressureRule: turnRule, + turnPressureFraction: turnFraction, + clearTurnPressureRule: clearTurnRule, + isDirty: true, + updatedAt: DateTime.now(), + ); + } + // -------------------------------------------------------------------------- // Repetitive Dive Support // -------------------------------------------------------------------------- diff --git a/lib/features/planner/domain/entities/plan_outcome.dart b/lib/features/planner/domain/entities/plan_outcome.dart index 2f3c2bcbc..6f6f93946 100644 --- a/lib/features/planner/domain/entities/plan_outcome.dart +++ b/lib/features/planner/domain/entities/plan_outcome.dart @@ -20,6 +20,7 @@ enum PlanIssueType { gasOut, ndlExceededNoDecoGas, noBailoutCarried, + minGasViolation, } /// One issue found while computing a plan. @@ -137,12 +138,20 @@ class PlanTankUsage extends Equatable { final double percentUsed; final bool reserveViolation; + /// Turn pressure per the plan's turn-pressure rule (bottom tanks only). + final double? turnPressureBar; + + /// Rock-bottom minimum gas for a stressed shared ascent (bottom tanks). + final double? minGasBar; + const PlanTankUsage({ required this.tankId, required this.litersUsed, this.remainingPressure, required this.percentUsed, this.reserveViolation = false, + this.turnPressureBar, + this.minGasBar, }); @override @@ -152,6 +161,8 @@ class PlanTankUsage extends Equatable { remainingPressure, percentUsed, reserveViolation, + turnPressureBar, + minGasBar, ]; } diff --git a/lib/features/planner/domain/services/contingency_service.dart b/lib/features/planner/domain/services/contingency_service.dart new file mode 100644 index 000000000..6a46df32d --- /dev/null +++ b/lib/features/planner/domain/services/contingency_service.dart @@ -0,0 +1,137 @@ +import 'package:submersion/core/constants/enums.dart'; +import 'package:submersion/features/dive_log/domain/entities/dive.dart'; +import 'package:submersion/features/dive_planner/domain/entities/plan_segment.dart'; +import 'package:submersion/features/planner/domain/entities/dive_plan.dart' + as domain; +import 'package:submersion/features/planner/domain/entities/plan_outcome.dart'; +import 'package:submersion/features/planner/domain/services/plan_engine.dart'; + +/// A "what if it goes deeper/longer" variant of the base plan. +class DeviationOutcome { + /// 'deeper' | 'longer' | 'both' + final String key; + final domain.DivePlan plan; + final PlanOutcome outcome; + + const DeviationOutcome({ + required this.key, + required this.plan, + required this.outcome, + }); +} + +/// The schedule that results from losing one deco/stage cylinder. +class LostGasOutcome { + final DiveTank tank; + final PlanOutcome outcome; + + const LostGasOutcome({required this.tank, required this.outcome}); +} + +/// Derives contingency variants of a plan and runs them through the +/// PlanEngine: the classic slate trio (+depth, +time, both) and one +/// lost-gas schedule per carried deco/stage cylinder. +class ContingencyService { + final PlanEngineConfig config; + + const ContingencyService({this.config = const PlanEngineConfig()}); + + PlanEngine get _engine => PlanEngine(config: config); + + /// The three deviation keys, in slate order. + static const deviationKeys = ['deeper', 'longer', 'both']; + + /// The deeper / longer / both variants (empty when the plan has no + /// segments). + List deviations(domain.DivePlan plan) { + if (plan.segments.isEmpty) return const []; + return [for (final key in deviationKeys) deviationFor(plan, key)!]; + } + + /// A single deviation variant by [key] ('deeper' | 'longer' | 'both'), or + /// null when the plan has no segments. Lets callers (the chart ghost) run + /// just the one variant the user selected instead of all three. + DeviationOutcome? deviationFor(domain.DivePlan plan, String key) { + if (plan.segments.isEmpty) return null; + final variant = switch (key) { + 'deeper' => _deepened(plan), + 'longer' => _lengthened(plan), + _ => _lengthened(_deepened(plan)), + }; + return DeviationOutcome( + key: key, + plan: variant, + outcome: _engine.compute(variant), + ); + } + + /// One outcome per lost deco/stage cylinder. Empty for CCR plans (loop + /// loss is the bailout solver's job) and when no such cylinder is carried. + List lostGas(domain.DivePlan plan) { + if (plan.mode == domain.PlanMode.ccr || plan.segments.isEmpty) { + return const []; + } + final results = []; + for (final tank in plan.tanks) { + if (tank.role != TankRole.deco && tank.role != TankRole.stage) continue; + final remaining = plan.tanks.where((t) => t.id != tank.id).toList(); + // Nothing left to breathe — a lost-gas schedule would be meaningless. + if (remaining.isEmpty) continue; + // Any user segment that breathed the lost cylinder is remapped onto a + // fallback (prefer back gas). Without this the contingency would still + // "breathe" the lost gas and its consumption would go unaccounted, since + // the engine only reports usage for tanks present in plan.tanks. + final fallback = remaining.firstWhere( + (t) => t.role == TankRole.backGas, + orElse: () => remaining.first, + ); + final without = plan.copyWith( + tanks: remaining, + segments: [ + for (final segment in plan.segments) + segment.tankId == tank.id + ? segment.copyWith(tankId: fallback.id, gasMix: fallback.gasMix) + : segment, + ], + ); + results.add( + LostGasOutcome(tank: tank, outcome: _engine.compute(without)), + ); + } + return results; + } + + /// Depths equal to the plan's max depth grow by the deviation delta — + /// the bottom deepens and the descent that feeds it follows. + domain.DivePlan _deepened(domain.DivePlan plan) { + final maxDepth = plan.maxDepth; + final delta = plan.deviationDepthDelta; + PlanSegment deepen(PlanSegment segment) { + var changed = segment; + if ((segment.startDepth - maxDepth).abs() < 0.01) { + changed = changed.copyWith(startDepth: segment.startDepth + delta); + } + if ((segment.endDepth - maxDepth).abs() < 0.01) { + changed = changed.copyWith(endDepth: segment.endDepth + delta); + } + return changed; + } + + return plan.copyWith(segments: plan.segments.map(deepen).toList()); + } + + /// Bottom segments grow by the deviation minutes. + domain.DivePlan _lengthened(domain.DivePlan plan) { + final extraSeconds = plan.deviationTimeMinutes * 60; + return plan.copyWith( + segments: [ + for (final segment in plan.segments) + segment.type == SegmentType.bottom + ? segment.copyWith( + durationSeconds: segment.durationSeconds + extraSeconds, + ) + : segment, + ], + ); + } +} diff --git a/lib/features/planner/domain/services/dive_plan_state_mapper.dart b/lib/features/planner/domain/services/dive_plan_state_mapper.dart index 190b5f307..d3c9ae553 100644 --- a/lib/features/planner/domain/services/dive_plan_state_mapper.dart +++ b/lib/features/planner/domain/services/dive_plan_state_mapper.dart @@ -38,6 +38,12 @@ domain.DivePlan divePlanFromState( clearSetpointHigh: state.setpointHigh == null, setpointSwitchDepth: state.setpointSwitchDepth, clearSetpointSwitchDepth: state.setpointSwitchDepth == null, + deviationDepthDelta: state.deviationDepthDelta, + deviationTimeMinutes: state.deviationTimeMinutes, + turnPressureRule: state.turnPressureRule, + clearTurnPressureRule: state.turnPressureRule == null, + turnPressureFraction: state.turnPressureFraction, + clearTurnPressureFraction: state.turnPressureFraction == null, gfLow: state.gfLow, gfHigh: state.gfHigh, sacBottom: state.sacRate, @@ -63,6 +69,10 @@ DivePlanState stateFromDivePlan(domain.DivePlan plan) { setpointLow: plan.setpointLow, setpointHigh: plan.setpointHigh, setpointSwitchDepth: plan.setpointSwitchDepth, + deviationDepthDelta: plan.deviationDepthDelta, + deviationTimeMinutes: plan.deviationTimeMinutes, + turnPressureRule: plan.turnPressureRule, + turnPressureFraction: plan.turnPressureFraction, gfLow: plan.gfLow, gfHigh: plan.gfHigh, sacRate: plan.sacBottom, diff --git a/lib/features/planner/domain/services/plan_engine.dart b/lib/features/planner/domain/services/plan_engine.dart index bf918dcdb..aa1657d2c 100644 --- a/lib/features/planner/domain/services/plan_engine.dart +++ b/lib/features/planner/domain/services/plan_engine.dart @@ -29,6 +29,9 @@ class PlanEngineConfig { /// CCR loop + counterlung volume, for descent diluent consumption. final double loopVolumeLiters; + /// Rock-bottom sizing: how many divers share the emergency ascent. + final double buddyFactor; + const PlanEngineConfig({ this.ppO2Working = 1.4, this.ppO2Deco = 1.6, @@ -38,6 +41,7 @@ class PlanEngineConfig { this.otuLimit = 300.0, this.o2MetabolicRateLpm = 1.0, this.loopVolumeLiters = 6.0, + this.buddyFactor = 2.0, }); } @@ -453,11 +457,55 @@ class PlanEngine { : 0.0, reserveViolation: remaining != null && remaining < plan.reservePressure, + turnPressureBar: _turnPressureFor(plan, tank), + minGasBar: _minGasFor(plan, tank, environment), ); }(), ]; } + /// Turn pressure per the plan's rule; bottom tanks with a start pressure + /// only. `usable = start - reserve`; turn when the rule's fraction of the + /// usable gas is consumed. + double? _turnPressureFor(domain.DivePlan plan, DiveTank tank) { + final rule = plan.turnPressureRule; + final start = tank.startPressure; + if (rule == null || start == null || tank.role != TankRole.backGas) { + return null; + } + final usable = (start - plan.reservePressure).clamp(0.0, start); + final fraction = switch (rule) { + domain.TurnPressureRule.allUsable => 1.0, + domain.TurnPressureRule.halves => 0.5, + domain.TurnPressureRule.thirds => 1.0 / 3.0, + domain.TurnPressureRule.custom => plan.turnPressureFraction ?? 1.0 / 3.0, + }; + return start - usable * fraction; + } + + /// Rock-bottom minimum gas: a stressed, buddy-shared emergency exit from + /// the plan's max depth — one minute at depth plus a direct ascent at the + /// plan rate — expressed as bar on this tank (bottom tanks, OC only). + double? _minGasFor( + domain.DivePlan plan, + DiveTank tank, + DiveEnvironment environment, + ) { + if (plan.mode == domain.PlanMode.ccr) return null; + if (tank.role != TankRole.backGas || tank.startPressure == null) { + return null; + } + final maxDepth = plan.maxDepth; + if (maxDepth <= 0) return null; + final sac = plan.sacStressedEffective * config.buddyFactor; + final ascentMinutes = maxDepth / plan.ascentRate; + final liters = + sac * environment.pressureAtDepth(maxDepth) + + sac * ascentMinutes * environment.pressureAtDepth(maxDepth / 2.0); + final volume = tank.volume ?? 11.0; + return volume > 0 ? liters / volume : null; + } + List _computeIssues( domain.DivePlan plan, List segments, @@ -648,6 +696,21 @@ class PlanEngine { ), ); } + final minGas = usage.minGasBar; + if (minGas != null && remaining > 0 && remaining < minGas) { + issues.add( + PlanIssue( + type: PlanIssueType.minGasViolation, + severity: PlanIssueSeverity.alert, + message: + 'Tank ends below the rock-bottom minimum of ' + '${minGas.toStringAsFixed(0)} bar', + segmentId: usage.tankId, + value: remaining, + threshold: minGas, + ), + ); + } } final inDeco = segmentOutcomes.any((s) => s.inDeco); diff --git a/lib/features/planner/presentation/pages/plan_canvas_page.dart b/lib/features/planner/presentation/pages/plan_canvas_page.dart index fa30cddba..8ed0ecdf2 100644 --- a/lib/features/planner/presentation/pages/plan_canvas_page.dart +++ b/lib/features/planner/presentation/pages/plan_canvas_page.dart @@ -11,6 +11,8 @@ import 'package:submersion/features/planner/domain/entities/dive_plan.dart' as domain; import 'package:submersion/features/planner/presentation/providers/plan_canvas_providers.dart'; import 'package:submersion/features/planner/presentation/widgets/ccr_settings_section.dart'; +import 'package:submersion/features/planner/presentation/widgets/contingency_chips.dart'; +import 'package:submersion/features/planner/presentation/widgets/contingency_settings_section.dart'; import 'package:submersion/features/planner/presentation/widgets/plan_canvas_chart.dart'; import 'package:submersion/features/planner/presentation/widgets/plan_results_sheet.dart'; import 'package:submersion/features/planner/presentation/widgets/plan_status_chips.dart'; @@ -185,6 +187,10 @@ class _PlanCanvasPageState extends ConsumerState { padding: const EdgeInsets.symmetric(horizontal: 12), child: PlanStatusChips(onIssuesTap: _openResultsSheet), ), + const Padding( + padding: EdgeInsets.fromLTRB(12, 6, 12, 0), + child: ContingencyChips(), + ), const SizedBox(height: 8), Expanded( child: ListView( @@ -249,6 +255,7 @@ class _PlanCanvasPageState extends ConsumerState { if (ref.watch(divePlanNotifierProvider).mode == domain.PlanMode.ccr) const CcrSettingsSection(), + const ContingencySettingsSection(), const SizedBox(height: 12), const PlanTankList(), const SizedBox(height: 12), @@ -270,6 +277,10 @@ class _PlanCanvasPageState extends ConsumerState { padding: const EdgeInsets.symmetric(horizontal: 16), child: PlanStatusChips(onIssuesTap: _scrollWideToIssues), ), + const Padding( + padding: EdgeInsets.fromLTRB(16, 6, 16, 0), + child: ContingencyChips(), + ), SizedBox( height: 260, child: PlanResultsSheet(controller: _wideResultsController), @@ -333,6 +344,7 @@ class _PlanCanvasPageState extends ConsumerState { if (ref.watch(divePlanNotifierProvider).mode == domain.PlanMode.ccr) const CcrSettingsSection(), + const ContingencySettingsSection(), ], ), ), diff --git a/lib/features/planner/presentation/providers/plan_canvas_providers.dart b/lib/features/planner/presentation/providers/plan_canvas_providers.dart index c90e7ffdc..8f541b66b 100644 --- a/lib/features/planner/presentation/providers/plan_canvas_providers.dart +++ b/lib/features/planner/presentation/providers/plan_canvas_providers.dart @@ -4,6 +4,7 @@ import 'package:submersion/features/dive_planner/domain/entities/plan_segment.da import 'package:submersion/features/dive_planner/presentation/providers/dive_planner_providers.dart'; import 'package:submersion/features/planner/domain/entities/plan_outcome.dart'; import 'package:submersion/features/planner/domain/services/bailout_solver.dart'; +import 'package:submersion/features/planner/domain/services/contingency_service.dart'; import 'package:submersion/features/planner/domain/services/dive_plan_state_mapper.dart'; import 'package:submersion/features/planner/domain/services/plan_engine.dart'; import 'package:submersion/features/settings/presentation/providers/settings_providers.dart'; @@ -98,13 +99,21 @@ class PlanCanvasSeries { final planCanvasSeriesProvider = Provider((ref) { final state = ref.watch(divePlanNotifierProvider); final outcome = ref.watch(planOutcomeProvider); + return buildCanvasSeries(segments: state.segments, outcome: outcome); +}); - final segments = List.from(state.segments) +/// Builds the chart series for any (segments, outcome) pair — the live plan +/// or a contingency variant (ghost overlay). +PlanCanvasSeries buildCanvasSeries({ + required List segments, + required PlanOutcome outcome, +}) { + final sorted = List.from(segments) ..sort((a, b) => a.order.compareTo(b.order)); final profile = []; var t = 0.0; - for (final segment in segments) { + for (final segment in sorted) { if (profile.isEmpty || profile.last.depth != segment.startDepth) { profile.add(CanvasPoint(t, segment.startDepth)); } @@ -117,9 +126,9 @@ final planCanvasSeriesProvider = Provider((ref) { final stopLabels = []; double? previousFO2; double? previousFHe; - if (segments.isNotEmpty) { - previousFO2 = segments.last.gasMix.o2 / 100.0; - previousFHe = segments.last.gasMix.he / 100.0; + if (sorted.isNotEmpty) { + previousFO2 = sorted.last.gasMix.o2 / 100.0; + previousFHe = sorted.last.gasMix.he / 100.0; } for (final stop in outcome.stops) { profile.add( @@ -191,7 +200,7 @@ final planCanvasSeriesProvider = Provider((ref) { maxTimeSeconds: outcome.runtimeSeconds.toDouble(), maxDepth: outcome.maxDepth, ); -}); +} /// Worst-case bailout for the current CCR plan; null for OC plans or when /// no bailout-role tank is carried. Recomputed live with the plan. @@ -200,3 +209,50 @@ final planBailoutProvider = Provider((ref) { final solver = BailoutSolver(config: ref.watch(planEngineConfigProvider)); return solver.solve(divePlanFromState(state)); }); + +/// Deviation (deeper/longer/both) outcomes for the current plan. +/// Whether the results sheet's contingency section is expanded. The full +/// deviation + lost-gas tables (3 engine runs + one per deco/stage tank) are +/// only computed while this is true, so a collapsed sheet costs nothing. +final contingenciesExpandedProvider = StateProvider((_) => false); + +final planDeviationsProvider = Provider>((ref) { + if (!ref.watch(contingenciesExpandedProvider)) return const []; + final state = ref.watch(divePlanNotifierProvider); + final service = ContingencyService( + config: ref.watch(planEngineConfigProvider), + ); + return service.deviations(divePlanFromState(state)); +}); + +/// Lost-gas outcomes for the current OC plan. +final planLostGasProvider = Provider>((ref) { + if (!ref.watch(contingenciesExpandedProvider)) return const []; + final state = ref.watch(divePlanNotifierProvider); + final service = ContingencyService( + config: ref.watch(planEngineConfigProvider), + ); + return service.lostGas(divePlanFromState(state)); +}); + +/// Which deviation is ghosted on the chart ('deeper'|'longer'|'both'; null = +/// none). +final selectedDeviationProvider = StateProvider((_) => null); + +/// Chart series for the selected deviation, drawn as a ghost overlay. Runs +/// ONLY the one selected variant (not the full deviation set), so ghosting a +/// contingency from the chips costs a single engine run. +final deviationGhostSeriesProvider = Provider((ref) { + final key = ref.watch(selectedDeviationProvider); + if (key == null) return null; + final state = ref.watch(divePlanNotifierProvider); + final service = ContingencyService( + config: ref.watch(planEngineConfigProvider), + ); + final deviation = service.deviationFor(divePlanFromState(state), key); + if (deviation == null) return null; + return buildCanvasSeries( + segments: deviation.plan.segments, + outcome: deviation.outcome, + ); +}); diff --git a/lib/features/planner/presentation/widgets/contingency_chips.dart b/lib/features/planner/presentation/widgets/contingency_chips.dart new file mode 100644 index 000000000..0fcafa512 --- /dev/null +++ b/lib/features/planner/presentation/widgets/contingency_chips.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; + +import 'package:submersion/core/providers/provider.dart'; +import 'package:submersion/core/utils/unit_formatter.dart'; +import 'package:submersion/features/dive_planner/presentation/providers/dive_planner_providers.dart'; +import 'package:submersion/features/planner/presentation/providers/plan_canvas_providers.dart'; +import 'package:submersion/features/planner/presentation/widgets/plan_status_chips.dart'; +import 'package:submersion/features/settings/presentation/providers/settings_providers.dart'; +import 'package:submersion/l10n/l10n_extension.dart'; + +/// Selector for the ghosted contingency: Base / +depth / +time / both. +/// Hidden when the plan has no segments. +class ContingencyChips extends ConsumerWidget { + const ContingencyChips({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final state = ref.watch(divePlanNotifierProvider); + if (state.segments.isEmpty) return const SizedBox.shrink(); + final selected = ref.watch(selectedDeviationProvider); + final units = UnitFormatter(ref.watch(settingsProvider)); + + final depthLabel = + '+${units.formatDepth(state.deviationDepthDelta, decimals: 0)}'; + final timeLabel = '+${state.deviationTimeMinutes}′'; + + void select(String? key) => + ref.read(selectedDeviationProvider.notifier).state = key; + + Widget chip(String? key, String label) => InkWell( + borderRadius: BorderRadius.circular(12), + onTap: () => select(key), + child: PlanChip(label: label, emphasized: selected == key), + ); + + return Wrap( + spacing: 6, + runSpacing: 6, + children: [ + chip(null, context.l10n.plannerCanvas_contingency_base), + chip('deeper', depthLabel), + chip('longer', timeLabel), + chip('both', '$depthLabel $timeLabel'), + ], + ); + } +} diff --git a/lib/features/planner/presentation/widgets/contingency_settings_section.dart b/lib/features/planner/presentation/widgets/contingency_settings_section.dart new file mode 100644 index 000000000..c9db9d7fa --- /dev/null +++ b/lib/features/planner/presentation/widgets/contingency_settings_section.dart @@ -0,0 +1,174 @@ +import 'package:flutter/material.dart'; + +import 'package:submersion/core/providers/provider.dart'; +import 'package:submersion/core/utils/unit_formatter.dart'; +import 'package:submersion/features/dive_planner/presentation/providers/dive_planner_providers.dart'; +import 'package:submersion/features/planner/domain/entities/dive_plan.dart' + as domain; +import 'package:submersion/features/settings/presentation/providers/settings_providers.dart'; +import 'package:submersion/l10n/l10n_extension.dart'; + +/// Contingency configuration: deviation deltas and the turn-pressure rule. +class ContingencySettingsSection extends ConsumerStatefulWidget { + const ContingencySettingsSection({super.key}); + + @override + ConsumerState createState() => + _ContingencySettingsSectionState(); +} + +class _ContingencySettingsSectionState + extends ConsumerState { + late final TextEditingController _depthController; + late final TextEditingController _timeController; + late final TextEditingController _fractionController; + + @override + void initState() { + super.initState(); + final state = ref.read(divePlanNotifierProvider); + final units = UnitFormatter(ref.read(settingsProvider)); + _depthController = TextEditingController( + text: units.convertDepth(state.deviationDepthDelta).toStringAsFixed(0), + ); + _timeController = TextEditingController( + text: state.deviationTimeMinutes.toString(), + ); + _fractionController = TextEditingController( + text: (state.turnPressureFraction ?? (1 / 3)).toStringAsFixed(2), + ); + } + + @override + void dispose() { + _depthController.dispose(); + _timeController.dispose(); + _fractionController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final state = ref.watch(divePlanNotifierProvider); + final units = UnitFormatter(ref.watch(settingsProvider)); + final notifier = ref.read(divePlanNotifierProvider.notifier); + final l10n = context.l10n; + + String ruleLabel(domain.TurnPressureRule? rule) => switch (rule) { + null => l10n.plannerCanvas_turnRule_none, + domain.TurnPressureRule.allUsable => + l10n.plannerCanvas_turnRule_allUsable, + domain.TurnPressureRule.halves => l10n.plannerCanvas_turnRule_halves, + domain.TurnPressureRule.thirds => l10n.plannerCanvas_turnRule_thirds, + domain.TurnPressureRule.custom => l10n.plannerCanvas_turnRule_custom, + }; + + return Padding( + padding: const EdgeInsets.only(top: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + l10n.plannerCanvas_contingency_title.toUpperCase(), + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: Theme.of(context).colorScheme.outline, + letterSpacing: 0.6, + ), + ), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: TextFormField( + controller: _depthController, + decoration: InputDecoration( + labelText: + '${l10n.plannerCanvas_contingency_depthDelta} ' + '(${units.depthSymbol})', + isDense: true, + border: const OutlineInputBorder(), + ), + keyboardType: TextInputType.number, + onChanged: (text) { + final parsed = double.tryParse(text); + if (parsed == null || parsed <= 0) return; + final factor = units.convertDepth(1.0); + notifier.updateContingencies( + depthDelta: factor > 0 ? parsed / factor : parsed, + ); + }, + ), + ), + const SizedBox(width: 8), + Expanded( + child: TextFormField( + controller: _timeController, + decoration: InputDecoration( + labelText: l10n.plannerCanvas_contingency_timeDelta, + isDense: true, + border: const OutlineInputBorder(), + ), + keyboardType: TextInputType.number, + onChanged: (text) { + final parsed = int.tryParse(text); + if (parsed == null || parsed <= 0) return; + notifier.updateContingencies(timeMinutes: parsed); + }, + ), + ), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: DropdownButtonFormField( + initialValue: state.turnPressureRule, + decoration: InputDecoration( + labelText: l10n.plannerCanvas_contingency_turnRule, + isDense: true, + border: const OutlineInputBorder(), + ), + items: [ + for (final rule in [ + null, + ...domain.TurnPressureRule.values, + ]) + DropdownMenuItem( + value: rule, + child: Text(ruleLabel(rule)), + ), + ], + onChanged: (rule) => rule == null + ? notifier.updateContingencies(clearTurnRule: true) + : notifier.updateContingencies(turnRule: rule), + ), + ), + if (state.turnPressureRule == domain.TurnPressureRule.custom) ...[ + const SizedBox(width: 8), + Expanded( + child: TextFormField( + controller: _fractionController, + decoration: InputDecoration( + labelText: l10n.plannerCanvas_contingency_turnFraction, + isDense: true, + border: const OutlineInputBorder(), + ), + keyboardType: const TextInputType.numberWithOptions( + decimal: true, + ), + onChanged: (text) { + final parsed = double.tryParse(text); + if (parsed == null || parsed <= 0 || parsed > 1) return; + notifier.updateContingencies(turnFraction: parsed); + }, + ), + ), + ], + ], + ), + ], + ), + ); + } +} diff --git a/lib/features/planner/presentation/widgets/plan_canvas_chart.dart b/lib/features/planner/presentation/widgets/plan_canvas_chart.dart index 608d5e75f..851866681 100644 --- a/lib/features/planner/presentation/widgets/plan_canvas_chart.dart +++ b/lib/features/planner/presentation/widgets/plan_canvas_chart.dart @@ -36,6 +36,7 @@ class PlanCanvasChart extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final series = ref.watch(planCanvasSeriesProvider); + final ghost = ref.watch(deviationGhostSeriesProvider); final scrubTime = ref.watch(scrubTimeProvider); final theme = Theme.of(context); final settings = ref.watch(settingsProvider); @@ -58,6 +59,7 @@ class PlanCanvasChart extends ConsumerWidget { context, ref, series, + ghost, scrubTime, maxDepthDisplay, maxTime, @@ -85,6 +87,7 @@ class PlanCanvasChart extends ConsumerWidget { BuildContext context, WidgetRef ref, PlanCanvasSeries series, + PlanCanvasSeries? ghost, double? scrubTime, double maxDepthDisplay, double maxTime, @@ -186,10 +189,32 @@ class PlanCanvasChart extends ConsumerWidget { ), extraLinesData: ExtraLinesData(verticalLines: verticalLines), minX: 0, - maxX: maxTime > 0 ? maxTime * 1.05 : 10, - minY: maxDepthDisplay > 0 ? -maxDepthDisplay * 1.1 : -10, + maxX: () { + var t = maxTime; + if (ghost != null && ghost.maxTimeSeconds / 60 > t) { + t = ghost.maxTimeSeconds / 60; + } + return t > 0 ? t * 1.05 : 10.0; + }(), + minY: () { + var d = maxDepthDisplay; + if (ghost != null && units.convertDepth(ghost.maxDepth) > d) { + d = units.convertDepth(ghost.maxDepth); + } + return d > 0 ? -d * 1.1 : -10.0; + }(), maxY: 0, lineBarsData: [ + // Ghosted contingency profile (grey dashed, under everything). + if (ghost != null && ghost.profile.length >= 2) + LineChartBarData( + spots: ghost.profile.map(toSpot).toList(), + isCurved: false, + color: theme.colorScheme.outline.withValues(alpha: 0.6), + barWidth: 1.5, + dashArray: const [5, 4], + dotData: const FlDotData(show: false), + ), // Ceiling overlay (dashed, drawn under the profile line). if (ceilingSpots.length >= 2) LineChartBarData( diff --git a/lib/features/planner/presentation/widgets/plan_results_sheet.dart b/lib/features/planner/presentation/widgets/plan_results_sheet.dart index 2adc98948..032ce977e 100644 --- a/lib/features/planner/presentation/widgets/plan_results_sheet.dart +++ b/lib/features/planner/presentation/widgets/plan_results_sheet.dart @@ -64,6 +64,10 @@ String planIssueMessage( return l10n.plannerCanvas_issue_noDecoGas; case PlanIssueType.noBailoutCarried: return l10n.plannerCanvas_issue_noBailout; + case PlanIssueType.minGasViolation: + return l10n.plannerCanvas_issue_minGas( + units.formatPressure(issue.threshold ?? 0), + ); } } @@ -120,6 +124,7 @@ class PlanResultsSheet extends ConsumerWidget { _SectionHeader(context.l10n.plannerCanvas_bailout_title), _BailoutSection(outcome: bailout, units: units), ], + ...?_contingencySections(context, ref, units), const SizedBox(height: 20), _SectionHeader(context.l10n.divePlanner_label_warnings), if (outcome.issues.isEmpty) @@ -139,6 +144,77 @@ class PlanResultsSheet extends ConsumerWidget { ); } + /// Collapsible deviation and lost-gas mini tables; null when the plan has no + /// segments to vary. Collapsed by default so the expensive per-variant engine + /// runs happen only when the diver opens the section (the providers return + /// empty while [contingenciesExpandedProvider] is false). + List? _contingencySections( + BuildContext context, + WidgetRef ref, + UnitFormatter units, + ) { + final state = ref.watch(divePlanNotifierProvider); + if (state.segments.isEmpty) return null; + final expanded = ref.watch(contingenciesExpandedProvider); + final deviations = ref.watch(planDeviationsProvider); + final lostGas = ref.watch(planLostGasProvider); + final theme = Theme.of(context); + + String deviationLabel(String key) { + final depth = + '+${units.formatDepth(state.deviationDepthDelta, decimals: 0)}'; + final time = '+${state.deviationTimeMinutes}\u2032'; + return switch (key) { + 'deeper' => depth, + 'longer' => time, + _ => '$depth $time', + }; + } + + Widget subHeader(String text) => Padding( + padding: const EdgeInsets.only(top: 10, bottom: 4), + child: Text( + text, + style: theme.textTheme.labelMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + ); + + return [ + const SizedBox(height: 20), + InkWell( + onTap: () => + ref.read(contingenciesExpandedProvider.notifier).state = !expanded, + child: Row( + children: [ + _SectionHeader(context.l10n.plannerCanvas_contingency_title), + const Spacer(), + Icon( + expanded ? Icons.expand_less : Icons.expand_more, + size: 20, + color: theme.colorScheme.outline, + ), + ], + ), + ), + if (expanded) ...[ + for (final deviation in deviations) ...[ + subHeader(deviationLabel(deviation.key)), + _RuntimeTable(outcome: deviation.outcome, units: units), + ], + for (final lost in lostGas) ...[ + subHeader( + context.l10n.plannerCanvas_contingency_lostGas( + lost.tank.gasMix.name, + ), + ), + _RuntimeTable(outcome: lost.outcome, units: units), + ], + ], + ]; + } + Widget _grip(ThemeData theme) => Center( child: Container( width: 32, @@ -281,6 +357,25 @@ class _GasRow extends StatelessWidget { ), ], ), + if (usage.turnPressureBar != null || usage.minGasBar != null) + Padding( + padding: const EdgeInsets.only(top: 2), + child: Text( + [ + if (usage.turnPressureBar != null) + context.l10n.plannerCanvas_gas_turnAt( + units.formatPressure(usage.turnPressureBar), + ), + if (usage.minGasBar != null) + context.l10n.plannerCanvas_gas_minGas( + units.formatPressure(usage.minGasBar), + ), + ].join(' \u00b7 '), + style: theme.textTheme.labelSmall?.copyWith( + color: theme.colorScheme.outline, + ), + ), + ), const SizedBox(height: 4), ClipRRect( borderRadius: BorderRadius.circular(3), diff --git a/lib/l10n/arb/app_ar.arb b/lib/l10n/arb/app_ar.arb index e0cd70a7a..a86afcb64 100644 --- a/lib/l10n/arb/app_ar.arb +++ b/lib/l10n/arb/app_ar.arb @@ -3247,9 +3247,19 @@ "plannerCanvas_ccr_switchDepth": "عمق تبديل نقطة الضبط", "plannerCanvas_chip_cns": "CNS {value}%", "plannerCanvas_chip_issues": "{count, plural, =1{مشكلة واحدة} other{{count} مشكلات}}", + "plannerCanvas_contingency_base": "أساسي", + "plannerCanvas_contingency_depthDelta": "عمق إضافي", + "plannerCanvas_contingency_lostGas": "فقدان {gas}", + "plannerCanvas_contingency_timeDelta": "دقائق إضافية", + "plannerCanvas_contingency_title": "خطط الطوارئ", + "plannerCanvas_contingency_turnFraction": "نسبة العودة", + "plannerCanvas_contingency_turnRule": "قاعدة ضغط العودة", + "plannerCanvas_gas_minGas": "الحد الأدنى للغاز {pressure}", + "plannerCanvas_gas_turnAt": "العودة عند {pressure}", "plannerCanvas_issue_gasDensityCritical": "كثافة الغاز {value} g/L تتجاوز الحد الأقصى", "plannerCanvas_issue_gasDensityHigh": "كثافة الغاز {value} g/L تتجاوز الحد الموصى به", "plannerCanvas_issue_hypoxic": "غاز ناقص الأكسجين عند {depth} (ppO₂ {value} bar)", + "plannerCanvas_issue_minGas": "تنتهي الأسطوانة تحت الحد الأدنى الآمن {pressure}", "plannerCanvas_issue_noBailout": "خطة تخفيف الضغط CCR لا تتضمن غاز إنقاذ (bailout)", "plannerCanvas_issue_noDecoGas": "يلزم تخفيف الضغط ولكن لا يوجد غاز تخفيف", "plannerCanvas_results_noDeco": "لا يلزم تخفيف الضغط", @@ -3264,6 +3274,11 @@ "plannerCanvas_table_gas": "الغاز", "plannerCanvas_table_runtime": "RT", "plannerCanvas_table_stop": "توقف", + "plannerCanvas_turnRule_allUsable": "كل القابل للاستخدام", + "plannerCanvas_turnRule_custom": "مخصص", + "plannerCanvas_turnRule_halves": "أنصاف", + "plannerCanvas_turnRule_none": "بدون", + "plannerCanvas_turnRule_thirds": "أثلاث", "planning_appBar_title": "التخطيط", "planning_card_decoCalculator_description": "احسب حدود عدم تخفيف الضغط، ومحطات تخفيف الضغط المطلوبة، والتعرض لـ CNS/OTU لملفات الغوص متعددة المستويات.", "planning_card_decoCalculator_subtitle": "خطط للغوصات مع محطات تخفيف الضغط", diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index ace15dc57..b4d39a933 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -3247,9 +3247,19 @@ "plannerCanvas_ccr_switchDepth": "Setpoint-Wechseltiefe", "plannerCanvas_chip_cns": "CNS {value}%", "plannerCanvas_chip_issues": "{count, plural, =1{1 Problem} other{{count} Probleme}}", + "plannerCanvas_contingency_base": "Basis", + "plannerCanvas_contingency_depthDelta": "Zusatztiefe", + "plannerCanvas_contingency_lostGas": "{gas} verloren", + "plannerCanvas_contingency_timeDelta": "Zusatzminuten", + "plannerCanvas_contingency_title": "Eventualpläne", + "plannerCanvas_contingency_turnFraction": "Umkehranteil", + "plannerCanvas_contingency_turnRule": "Umkehrdruck-Regel", + "plannerCanvas_gas_minGas": "Min-Gas {pressure}", + "plannerCanvas_gas_turnAt": "Umkehr bei {pressure}", "plannerCanvas_issue_gasDensityCritical": "Gasdichte {value} g/L über hartem Limit", "plannerCanvas_issue_gasDensityHigh": "Gasdichte {value} g/L über empfohlenem Limit", "plannerCanvas_issue_hypoxic": "Hypoxisches Gas bei {depth} (ppO₂ {value} bar)", + "plannerCanvas_issue_minGas": "Flasche endet unter dem Rock-Bottom-Minimum von {pressure}", "plannerCanvas_issue_noBailout": "CCR-Dekompressionsplan führt kein Bailout-Gas mit", "plannerCanvas_issue_noDecoGas": "Dekompression erforderlich, aber kein Deko-Gas mitgeführt", "plannerCanvas_results_noDeco": "Keine Dekompression erforderlich", @@ -3264,6 +3274,11 @@ "plannerCanvas_table_gas": "Gas", "plannerCanvas_table_runtime": "RT", "plannerCanvas_table_stop": "Stopp", + "plannerCanvas_turnRule_allUsable": "Alles nutzbar", + "plannerCanvas_turnRule_custom": "Benutzerdefiniert", + "plannerCanvas_turnRule_halves": "Hälften", + "plannerCanvas_turnRule_none": "Keine", + "plannerCanvas_turnRule_thirds": "Drittel", "planning_appBar_title": "Planung", "planning_card_decoCalculator_description": "Berechnen Sie Nullzeitgrenzen, erforderliche Dekostopps und CNS/OTU-Belastung für mehrstufige Tauchprofile.", "planning_card_decoCalculator_subtitle": "Tauchgänge mit Dekostopps planen", diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index f471efa89..fea383246 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -5678,6 +5678,30 @@ "count": { "type": "int" } } }, + "plannerCanvas_contingency_base": "Base", + "plannerCanvas_contingency_depthDelta": "Extra depth", + "plannerCanvas_contingency_lostGas": "Lost {gas}", + "@plannerCanvas_contingency_lostGas": { + "placeholders": { + "gas": { "type": "String" } + } + }, + "plannerCanvas_contingency_timeDelta": "Extra minutes", + "plannerCanvas_contingency_title": "Contingencies", + "plannerCanvas_contingency_turnFraction": "Turn fraction", + "plannerCanvas_contingency_turnRule": "Turn pressure rule", + "plannerCanvas_gas_minGas": "min gas {pressure}", + "@plannerCanvas_gas_minGas": { + "placeholders": { + "pressure": { "type": "String" } + } + }, + "plannerCanvas_gas_turnAt": "turn @ {pressure}", + "@plannerCanvas_gas_turnAt": { + "placeholders": { + "pressure": { "type": "String" } + } + }, "plannerCanvas_issue_gasDensityCritical": "Gas density {value} g/L over hard limit", "@plannerCanvas_issue_gasDensityCritical": { "placeholders": { @@ -5697,6 +5721,12 @@ "value": { "type": "String" } } }, + "plannerCanvas_issue_minGas": "Tank ends below the rock-bottom minimum of {pressure}", + "@plannerCanvas_issue_minGas": { + "placeholders": { + "pressure": { "type": "String" } + } + }, "plannerCanvas_issue_noBailout": "CCR decompression plan carries no bailout gas", "plannerCanvas_issue_noDecoGas": "Decompression required but no deco gas carried", "plannerCanvas_results_noDeco": "No decompression required", @@ -5727,6 +5757,11 @@ "plannerCanvas_table_gas": "Gas", "plannerCanvas_table_runtime": "RT", "plannerCanvas_table_stop": "Stop", + "plannerCanvas_turnRule_allUsable": "All usable", + "plannerCanvas_turnRule_custom": "Custom", + "plannerCanvas_turnRule_halves": "Halves", + "plannerCanvas_turnRule_none": "None", + "plannerCanvas_turnRule_thirds": "Thirds", "planning_appBar_title": "Planning", "planning_card_decoCalculator_description": "Calculate no-decompression limits, required deco stops, and CNS/OTU exposure for multi-level dive profiles.", "planning_card_decoCalculator_subtitle": "Plan dives with decompression stops", diff --git a/lib/l10n/arb/app_es.arb b/lib/l10n/arb/app_es.arb index ee92538a8..5c8f1aab4 100644 --- a/lib/l10n/arb/app_es.arb +++ b/lib/l10n/arb/app_es.arb @@ -3247,9 +3247,19 @@ "plannerCanvas_ccr_switchDepth": "Profundidad de cambio de setpoint", "plannerCanvas_chip_cns": "CNS {value}%", "plannerCanvas_chip_issues": "{count, plural, =1{1 problema} other{{count} problemas}}", + "plannerCanvas_contingency_base": "Base", + "plannerCanvas_contingency_depthDelta": "Profundidad extra", + "plannerCanvas_contingency_lostGas": "{gas} perdido", + "plannerCanvas_contingency_timeDelta": "Minutos extra", + "plannerCanvas_contingency_title": "Contingencias", + "plannerCanvas_contingency_turnFraction": "Fracción de retorno", + "plannerCanvas_contingency_turnRule": "Regla de presión de retorno", + "plannerCanvas_gas_minGas": "gas mínimo {pressure}", + "plannerCanvas_gas_turnAt": "retorno a {pressure}", "plannerCanvas_issue_gasDensityCritical": "Densidad del gas {value} g/L sobre el límite máximo", "plannerCanvas_issue_gasDensityHigh": "Densidad del gas {value} g/L sobre el límite recomendado", "plannerCanvas_issue_hypoxic": "Gas hipóxico a {depth} (ppO₂ {value} bar)", + "plannerCanvas_issue_minGas": "La botella termina por debajo del mínimo rock bottom de {pressure}", "plannerCanvas_issue_noBailout": "El plan de descompresión CCR no lleva gas de bailout", "plannerCanvas_issue_noDecoGas": "Se requiere descompresión pero no se lleva gas de deco", "plannerCanvas_results_noDeco": "No se requiere descompresión", @@ -3264,6 +3274,11 @@ "plannerCanvas_table_gas": "Gas", "plannerCanvas_table_runtime": "RT", "plannerCanvas_table_stop": "Parada", + "plannerCanvas_turnRule_allUsable": "Todo utilizable", + "plannerCanvas_turnRule_custom": "Personalizada", + "plannerCanvas_turnRule_halves": "Mitades", + "plannerCanvas_turnRule_none": "Ninguna", + "plannerCanvas_turnRule_thirds": "Tercios", "planning_appBar_title": "Planificacion", "planning_card_decoCalculator_description": "Calcula los limites de no descompresion, las paradas de deco requeridas y la exposicion CNS/OTU para perfiles de inmersion multinivel.", "planning_card_decoCalculator_subtitle": "Planifica inmersiones con paradas de descompresion", diff --git a/lib/l10n/arb/app_fr.arb b/lib/l10n/arb/app_fr.arb index ded456a66..28b96d256 100644 --- a/lib/l10n/arb/app_fr.arb +++ b/lib/l10n/arb/app_fr.arb @@ -3213,9 +3213,19 @@ "plannerCanvas_ccr_switchDepth": "Profondeur de bascule du setpoint", "plannerCanvas_chip_cns": "CNS {value}%", "plannerCanvas_chip_issues": "{count, plural, =1{1 problème} other{{count} problèmes}}", + "plannerCanvas_contingency_base": "Base", + "plannerCanvas_contingency_depthDelta": "Profondeur en plus", + "plannerCanvas_contingency_lostGas": "{gas} perdu", + "plannerCanvas_contingency_timeDelta": "Minutes en plus", + "plannerCanvas_contingency_title": "Plans de secours", + "plannerCanvas_contingency_turnFraction": "Fraction de demi-tour", + "plannerCanvas_contingency_turnRule": "Règle de pression de demi-tour", + "plannerCanvas_gas_minGas": "gaz mini {pressure}", + "plannerCanvas_gas_turnAt": "demi-tour à {pressure}", "plannerCanvas_issue_gasDensityCritical": "Densité du gaz {value} g/L au-dessus de la limite stricte", "plannerCanvas_issue_gasDensityHigh": "Densité du gaz {value} g/L au-dessus de la limite recommandée", "plannerCanvas_issue_hypoxic": "Gaz hypoxique à {depth} (ppO₂ {value} bar)", + "plannerCanvas_issue_minGas": "Le bloc finit sous le minimum rock bottom de {pressure}", "plannerCanvas_issue_noBailout": "Le plan de décompression CCR n'emporte aucun gaz de bailout", "plannerCanvas_issue_noDecoGas": "Décompression requise mais aucun gaz de déco emporté", "plannerCanvas_results_noDeco": "Aucune décompression requise", @@ -3230,6 +3240,11 @@ "plannerCanvas_table_gas": "Gaz", "plannerCanvas_table_runtime": "RT", "plannerCanvas_table_stop": "Palier", + "plannerCanvas_turnRule_allUsable": "Tout utilisable", + "plannerCanvas_turnRule_custom": "Personnalisée", + "plannerCanvas_turnRule_halves": "Moitiés", + "plannerCanvas_turnRule_none": "Aucune", + "plannerCanvas_turnRule_thirds": "Tiers", "planning_appBar_title": "Planification", "planning_card_decoCalculator_description": "Calculez les limites de non-decompression, les paliers de decompression requis et l'exposition CNS/OTU pour des profils de plongee multi-niveaux.", "planning_card_decoCalculator_subtitle": "Planifiez des plongees avec paliers de decompression", diff --git a/lib/l10n/arb/app_he.arb b/lib/l10n/arb/app_he.arb index 2f3d6910d..cab2afd74 100644 --- a/lib/l10n/arb/app_he.arb +++ b/lib/l10n/arb/app_he.arb @@ -3213,9 +3213,19 @@ "plannerCanvas_ccr_switchDepth": "עומק החלפת נקודת הכיוון", "plannerCanvas_chip_cns": "CNS {value}%", "plannerCanvas_chip_issues": "{count, plural, =1{בעיה אחת} other{{count} בעיות}}", + "plannerCanvas_contingency_base": "בסיס", + "plannerCanvas_contingency_depthDelta": "עומק נוסף", + "plannerCanvas_contingency_lostGas": "אבדן {gas}", + "plannerCanvas_contingency_timeDelta": "דקות נוספות", + "plannerCanvas_contingency_title": "תוכניות חירום", + "plannerCanvas_contingency_turnFraction": "שבר פנייה", + "plannerCanvas_contingency_turnRule": "כלל לחץ פנייה", + "plannerCanvas_gas_minGas": "גז מינימלי {pressure}", + "plannerCanvas_gas_turnAt": "פנייה ב-{pressure}", "plannerCanvas_issue_gasDensityCritical": "צפיפות הגז {value} g/L מעל הגבול המרבי", "plannerCanvas_issue_gasDensityHigh": "צפיפות הגז {value} g/L מעל הגבול המומלץ", "plannerCanvas_issue_hypoxic": "גז היפוקסי ב-{depth} (ppO₂ {value} bar)", + "plannerCanvas_issue_minGas": "המיכל מסתיים מתחת למינימום rock bottom של {pressure}", "plannerCanvas_issue_noBailout": "תוכנית דקומפרסיה CCR ללא גז חילוץ (bailout)", "plannerCanvas_issue_noDecoGas": "נדרשת דקומפרסיה אך לא נלקח גז דקו", "plannerCanvas_results_noDeco": "לא נדרשת דקומפרסיה", @@ -3230,6 +3240,11 @@ "plannerCanvas_table_gas": "גז", "plannerCanvas_table_runtime": "RT", "plannerCanvas_table_stop": "עצירה", + "plannerCanvas_turnRule_allUsable": "הכול שמיש", + "plannerCanvas_turnRule_custom": "מותאם אישית", + "plannerCanvas_turnRule_halves": "חצאים", + "plannerCanvas_turnRule_none": "ללא", + "plannerCanvas_turnRule_thirds": "שלישים", "planning_appBar_title": "תכנון", "planning_card_decoCalculator_description": "חשב מגבלות ללא דקומפרסיה, עצירות דקו נדרשות וחשיפת CNS/OTU עבור פרופילי צלילה רב-שלביים.", "planning_card_decoCalculator_subtitle": "תכנן צלילות עם עצירות דקומפרסיה", diff --git a/lib/l10n/arb/app_hu.arb b/lib/l10n/arb/app_hu.arb index b39431e41..a5c680c95 100644 --- a/lib/l10n/arb/app_hu.arb +++ b/lib/l10n/arb/app_hu.arb @@ -3213,9 +3213,19 @@ "plannerCanvas_ccr_switchDepth": "Setpoint váltási mélység", "plannerCanvas_chip_cns": "CNS {value}%", "plannerCanvas_chip_issues": "{count, plural, =1{1 probléma} other{{count} probléma}}", + "plannerCanvas_contingency_base": "Alap", + "plannerCanvas_contingency_depthDelta": "Extra mélység", + "plannerCanvas_contingency_lostGas": "{gas} elveszett", + "plannerCanvas_contingency_timeDelta": "Extra percek", + "plannerCanvas_contingency_title": "Vészhelyzeti tervek", + "plannerCanvas_contingency_turnFraction": "Fordulási hányad", + "plannerCanvas_contingency_turnRule": "Fordulónyomás-szabály", + "plannerCanvas_gas_minGas": "min. gáz {pressure}", + "plannerCanvas_gas_turnAt": "fordulás {pressure}-nál", "plannerCanvas_issue_gasDensityCritical": "Gázsűrűség {value} g/L a szigorú határ felett", "plannerCanvas_issue_gasDensityHigh": "Gázsűrűség {value} g/L az ajánlott határ felett", "plannerCanvas_issue_hypoxic": "Hipoxiás gáz {depth} mélységben (ppO₂ {value} bar)", + "plannerCanvas_issue_minGas": "A palack a rock-bottom minimum ({pressure}) alatt végez", "plannerCanvas_issue_noBailout": "A CCR dekompressziós terv nem tartalmaz bailout gázt", "plannerCanvas_issue_noDecoGas": "Dekompresszió szükséges, de nincs deco gáz", "plannerCanvas_results_noDeco": "Nincs szükség dekompresszióra", @@ -3230,6 +3240,11 @@ "plannerCanvas_table_gas": "Gáz", "plannerCanvas_table_runtime": "RT", "plannerCanvas_table_stop": "Megálló", + "plannerCanvas_turnRule_allUsable": "Minden felhasználható", + "plannerCanvas_turnRule_custom": "Egyéni", + "plannerCanvas_turnRule_halves": "Felek", + "plannerCanvas_turnRule_none": "Nincs", + "plannerCanvas_turnRule_thirds": "Harmadok", "planning_appBar_title": "Tervezes", "planning_card_decoCalculator_description": "Szamitsa ki a dekompresszios limiteket, a szükseges deko megalloket es a CNS/OTU terhelest többszintu merülesi profilokhoz.", "planning_card_decoCalculator_subtitle": "Merülesek tervezese dekompressziós megallokkal", diff --git a/lib/l10n/arb/app_it.arb b/lib/l10n/arb/app_it.arb index 38a1cf615..7a22bb58c 100644 --- a/lib/l10n/arb/app_it.arb +++ b/lib/l10n/arb/app_it.arb @@ -3213,9 +3213,19 @@ "plannerCanvas_ccr_switchDepth": "Profondità di cambio setpoint", "plannerCanvas_chip_cns": "CNS {value}%", "plannerCanvas_chip_issues": "{count, plural, =1{1 problema} other{{count} problemi}}", + "plannerCanvas_contingency_base": "Base", + "plannerCanvas_contingency_depthDelta": "Profondità extra", + "plannerCanvas_contingency_lostGas": "{gas} perso", + "plannerCanvas_contingency_timeDelta": "Minuti extra", + "plannerCanvas_contingency_title": "Contingenze", + "plannerCanvas_contingency_turnFraction": "Frazione di rientro", + "plannerCanvas_contingency_turnRule": "Regola pressione di rientro", + "plannerCanvas_gas_minGas": "gas minimo {pressure}", + "plannerCanvas_gas_turnAt": "rientro a {pressure}", "plannerCanvas_issue_gasDensityCritical": "Densità del gas {value} g/L oltre il limite massimo", "plannerCanvas_issue_gasDensityHigh": "Densità del gas {value} g/L oltre il limite consigliato", "plannerCanvas_issue_hypoxic": "Gas ipossico a {depth} (ppO₂ {value} bar)", + "plannerCanvas_issue_minGas": "La bombola termina sotto il minimo rock bottom di {pressure}", "plannerCanvas_issue_noBailout": "Il piano di decompressione CCR non prevede gas di bailout", "plannerCanvas_issue_noDecoGas": "Decompressione necessaria ma nessun gas deco a bordo", "plannerCanvas_results_noDeco": "Nessuna decompressione necessaria", @@ -3230,6 +3240,11 @@ "plannerCanvas_table_gas": "Gas", "plannerCanvas_table_runtime": "RT", "plannerCanvas_table_stop": "Sosta", + "plannerCanvas_turnRule_allUsable": "Tutto utilizzabile", + "plannerCanvas_turnRule_custom": "Personalizzata", + "plannerCanvas_turnRule_halves": "Metà", + "plannerCanvas_turnRule_none": "Nessuna", + "plannerCanvas_turnRule_thirds": "Terzi", "planning_appBar_title": "Pianificazione", "planning_card_decoCalculator_description": "Calcola i limiti di non decompressione, le soste deco necessarie e l'esposizione CNS/OTU per profili di immersione multilivello.", "planning_card_decoCalculator_subtitle": "Pianifica immersioni con soste di decompressione", diff --git a/lib/l10n/arb/app_localizations.dart b/lib/l10n/arb/app_localizations.dart index 208af92c1..9ec3e1d07 100644 --- a/lib/l10n/arb/app_localizations.dart +++ b/lib/l10n/arb/app_localizations.dart @@ -17726,6 +17726,60 @@ abstract class AppLocalizations { /// **'{count, plural, =1{1 issue} other{{count} issues}}'** String plannerCanvas_chip_issues(int count); + /// No description provided for @plannerCanvas_contingency_base. + /// + /// In en, this message translates to: + /// **'Base'** + String get plannerCanvas_contingency_base; + + /// No description provided for @plannerCanvas_contingency_depthDelta. + /// + /// In en, this message translates to: + /// **'Extra depth'** + String get plannerCanvas_contingency_depthDelta; + + /// No description provided for @plannerCanvas_contingency_lostGas. + /// + /// In en, this message translates to: + /// **'Lost {gas}'** + String plannerCanvas_contingency_lostGas(String gas); + + /// No description provided for @plannerCanvas_contingency_timeDelta. + /// + /// In en, this message translates to: + /// **'Extra minutes'** + String get plannerCanvas_contingency_timeDelta; + + /// No description provided for @plannerCanvas_contingency_title. + /// + /// In en, this message translates to: + /// **'Contingencies'** + String get plannerCanvas_contingency_title; + + /// No description provided for @plannerCanvas_contingency_turnFraction. + /// + /// In en, this message translates to: + /// **'Turn fraction'** + String get plannerCanvas_contingency_turnFraction; + + /// No description provided for @plannerCanvas_contingency_turnRule. + /// + /// In en, this message translates to: + /// **'Turn pressure rule'** + String get plannerCanvas_contingency_turnRule; + + /// No description provided for @plannerCanvas_gas_minGas. + /// + /// In en, this message translates to: + /// **'min gas {pressure}'** + String plannerCanvas_gas_minGas(String pressure); + + /// No description provided for @plannerCanvas_gas_turnAt. + /// + /// In en, this message translates to: + /// **'turn @ {pressure}'** + String plannerCanvas_gas_turnAt(String pressure); + /// No description provided for @plannerCanvas_issue_gasDensityCritical. /// /// In en, this message translates to: @@ -17744,6 +17798,12 @@ abstract class AppLocalizations { /// **'Hypoxic gas at {depth} (ppO₂ {value} bar)'** String plannerCanvas_issue_hypoxic(String depth, String value); + /// No description provided for @plannerCanvas_issue_minGas. + /// + /// In en, this message translates to: + /// **'Tank ends below the rock-bottom minimum of {pressure}'** + String plannerCanvas_issue_minGas(String pressure); + /// No description provided for @plannerCanvas_issue_noBailout. /// /// In en, this message translates to: @@ -17828,6 +17888,36 @@ abstract class AppLocalizations { /// **'Stop'** String get plannerCanvas_table_stop; + /// No description provided for @plannerCanvas_turnRule_allUsable. + /// + /// In en, this message translates to: + /// **'All usable'** + String get plannerCanvas_turnRule_allUsable; + + /// No description provided for @plannerCanvas_turnRule_custom. + /// + /// In en, this message translates to: + /// **'Custom'** + String get plannerCanvas_turnRule_custom; + + /// No description provided for @plannerCanvas_turnRule_halves. + /// + /// In en, this message translates to: + /// **'Halves'** + String get plannerCanvas_turnRule_halves; + + /// No description provided for @plannerCanvas_turnRule_none. + /// + /// In en, this message translates to: + /// **'None'** + String get plannerCanvas_turnRule_none; + + /// No description provided for @plannerCanvas_turnRule_thirds. + /// + /// In en, this message translates to: + /// **'Thirds'** + String get plannerCanvas_turnRule_thirds; + /// No description provided for @planning_appBar_title. /// /// In en, this message translates to: diff --git a/lib/l10n/arb/app_localizations_ar.dart b/lib/l10n/arb/app_localizations_ar.dart index 2729a2750..d1e75f21f 100644 --- a/lib/l10n/arb/app_localizations_ar.dart +++ b/lib/l10n/arb/app_localizations_ar.dart @@ -10219,6 +10219,39 @@ class AppLocalizationsAr extends AppLocalizations { return '$_temp0'; } + @override + String get plannerCanvas_contingency_base => 'أساسي'; + + @override + String get plannerCanvas_contingency_depthDelta => 'عمق إضافي'; + + @override + String plannerCanvas_contingency_lostGas(String gas) { + return 'فقدان $gas'; + } + + @override + String get plannerCanvas_contingency_timeDelta => 'دقائق إضافية'; + + @override + String get plannerCanvas_contingency_title => 'خطط الطوارئ'; + + @override + String get plannerCanvas_contingency_turnFraction => 'نسبة العودة'; + + @override + String get plannerCanvas_contingency_turnRule => 'قاعدة ضغط العودة'; + + @override + String plannerCanvas_gas_minGas(String pressure) { + return 'الحد الأدنى للغاز $pressure'; + } + + @override + String plannerCanvas_gas_turnAt(String pressure) { + return 'العودة عند $pressure'; + } + @override String plannerCanvas_issue_gasDensityCritical(String value) { return 'كثافة الغاز $value g/L تتجاوز الحد الأقصى'; @@ -10234,6 +10267,11 @@ class AppLocalizationsAr extends AppLocalizations { return 'غاز ناقص الأكسجين عند $depth (ppO₂ $value bar)'; } + @override + String plannerCanvas_issue_minGas(String pressure) { + return 'تنتهي الأسطوانة تحت الحد الأدنى الآمن $pressure'; + } + @override String get plannerCanvas_issue_noBailout => 'خطة تخفيف الضغط CCR لا تتضمن غاز إنقاذ (bailout)'; @@ -10284,6 +10322,21 @@ class AppLocalizationsAr extends AppLocalizations { @override String get plannerCanvas_table_stop => 'توقف'; + @override + String get plannerCanvas_turnRule_allUsable => 'كل القابل للاستخدام'; + + @override + String get plannerCanvas_turnRule_custom => 'مخصص'; + + @override + String get plannerCanvas_turnRule_halves => 'أنصاف'; + + @override + String get plannerCanvas_turnRule_none => 'بدون'; + + @override + String get plannerCanvas_turnRule_thirds => 'أثلاث'; + @override String get planning_appBar_title => 'التخطيط'; diff --git a/lib/l10n/arb/app_localizations_de.dart b/lib/l10n/arb/app_localizations_de.dart index be185f9f9..d76c115a7 100644 --- a/lib/l10n/arb/app_localizations_de.dart +++ b/lib/l10n/arb/app_localizations_de.dart @@ -10413,6 +10413,39 @@ class AppLocalizationsDe extends AppLocalizations { return '$_temp0'; } + @override + String get plannerCanvas_contingency_base => 'Basis'; + + @override + String get plannerCanvas_contingency_depthDelta => 'Zusatztiefe'; + + @override + String plannerCanvas_contingency_lostGas(String gas) { + return '$gas verloren'; + } + + @override + String get plannerCanvas_contingency_timeDelta => 'Zusatzminuten'; + + @override + String get plannerCanvas_contingency_title => 'Eventualpläne'; + + @override + String get plannerCanvas_contingency_turnFraction => 'Umkehranteil'; + + @override + String get plannerCanvas_contingency_turnRule => 'Umkehrdruck-Regel'; + + @override + String plannerCanvas_gas_minGas(String pressure) { + return 'Min-Gas $pressure'; + } + + @override + String plannerCanvas_gas_turnAt(String pressure) { + return 'Umkehr bei $pressure'; + } + @override String plannerCanvas_issue_gasDensityCritical(String value) { return 'Gasdichte $value g/L über hartem Limit'; @@ -10428,6 +10461,11 @@ class AppLocalizationsDe extends AppLocalizations { return 'Hypoxisches Gas bei $depth (ppO₂ $value bar)'; } + @override + String plannerCanvas_issue_minGas(String pressure) { + return 'Flasche endet unter dem Rock-Bottom-Minimum von $pressure'; + } + @override String get plannerCanvas_issue_noBailout => 'CCR-Dekompressionsplan führt kein Bailout-Gas mit'; @@ -10478,6 +10516,21 @@ class AppLocalizationsDe extends AppLocalizations { @override String get plannerCanvas_table_stop => 'Stopp'; + @override + String get plannerCanvas_turnRule_allUsable => 'Alles nutzbar'; + + @override + String get plannerCanvas_turnRule_custom => 'Benutzerdefiniert'; + + @override + String get plannerCanvas_turnRule_halves => 'Hälften'; + + @override + String get plannerCanvas_turnRule_none => 'Keine'; + + @override + String get plannerCanvas_turnRule_thirds => 'Drittel'; + @override String get planning_appBar_title => 'Planung'; diff --git a/lib/l10n/arb/app_localizations_en.dart b/lib/l10n/arb/app_localizations_en.dart index 14d26a48e..3e02f5658 100644 --- a/lib/l10n/arb/app_localizations_en.dart +++ b/lib/l10n/arb/app_localizations_en.dart @@ -10252,6 +10252,39 @@ class AppLocalizationsEn extends AppLocalizations { return '$_temp0'; } + @override + String get plannerCanvas_contingency_base => 'Base'; + + @override + String get plannerCanvas_contingency_depthDelta => 'Extra depth'; + + @override + String plannerCanvas_contingency_lostGas(String gas) { + return 'Lost $gas'; + } + + @override + String get plannerCanvas_contingency_timeDelta => 'Extra minutes'; + + @override + String get plannerCanvas_contingency_title => 'Contingencies'; + + @override + String get plannerCanvas_contingency_turnFraction => 'Turn fraction'; + + @override + String get plannerCanvas_contingency_turnRule => 'Turn pressure rule'; + + @override + String plannerCanvas_gas_minGas(String pressure) { + return 'min gas $pressure'; + } + + @override + String plannerCanvas_gas_turnAt(String pressure) { + return 'turn @ $pressure'; + } + @override String plannerCanvas_issue_gasDensityCritical(String value) { return 'Gas density $value g/L over hard limit'; @@ -10267,6 +10300,11 @@ class AppLocalizationsEn extends AppLocalizations { return 'Hypoxic gas at $depth (ppO₂ $value bar)'; } + @override + String plannerCanvas_issue_minGas(String pressure) { + return 'Tank ends below the rock-bottom minimum of $pressure'; + } + @override String get plannerCanvas_issue_noBailout => 'CCR decompression plan carries no bailout gas'; @@ -10317,6 +10355,21 @@ class AppLocalizationsEn extends AppLocalizations { @override String get plannerCanvas_table_stop => 'Stop'; + @override + String get plannerCanvas_turnRule_allUsable => 'All usable'; + + @override + String get plannerCanvas_turnRule_custom => 'Custom'; + + @override + String get plannerCanvas_turnRule_halves => 'Halves'; + + @override + String get plannerCanvas_turnRule_none => 'None'; + + @override + String get plannerCanvas_turnRule_thirds => 'Thirds'; + @override String get planning_appBar_title => 'Planning'; diff --git a/lib/l10n/arb/app_localizations_es.dart b/lib/l10n/arb/app_localizations_es.dart index 92c1f11e8..c43b6cb85 100644 --- a/lib/l10n/arb/app_localizations_es.dart +++ b/lib/l10n/arb/app_localizations_es.dart @@ -10402,6 +10402,40 @@ class AppLocalizationsEs extends AppLocalizations { return '$_temp0'; } + @override + String get plannerCanvas_contingency_base => 'Base'; + + @override + String get plannerCanvas_contingency_depthDelta => 'Profundidad extra'; + + @override + String plannerCanvas_contingency_lostGas(String gas) { + return '$gas perdido'; + } + + @override + String get plannerCanvas_contingency_timeDelta => 'Minutos extra'; + + @override + String get plannerCanvas_contingency_title => 'Contingencias'; + + @override + String get plannerCanvas_contingency_turnFraction => 'Fracción de retorno'; + + @override + String get plannerCanvas_contingency_turnRule => + 'Regla de presión de retorno'; + + @override + String plannerCanvas_gas_minGas(String pressure) { + return 'gas mínimo $pressure'; + } + + @override + String plannerCanvas_gas_turnAt(String pressure) { + return 'retorno a $pressure'; + } + @override String plannerCanvas_issue_gasDensityCritical(String value) { return 'Densidad del gas $value g/L sobre el límite máximo'; @@ -10417,6 +10451,11 @@ class AppLocalizationsEs extends AppLocalizations { return 'Gas hipóxico a $depth (ppO₂ $value bar)'; } + @override + String plannerCanvas_issue_minGas(String pressure) { + return 'La botella termina por debajo del mínimo rock bottom de $pressure'; + } + @override String get plannerCanvas_issue_noBailout => 'El plan de descompresión CCR no lleva gas de bailout'; @@ -10467,6 +10506,21 @@ class AppLocalizationsEs extends AppLocalizations { @override String get plannerCanvas_table_stop => 'Parada'; + @override + String get plannerCanvas_turnRule_allUsable => 'Todo utilizable'; + + @override + String get plannerCanvas_turnRule_custom => 'Personalizada'; + + @override + String get plannerCanvas_turnRule_halves => 'Mitades'; + + @override + String get plannerCanvas_turnRule_none => 'Ninguna'; + + @override + String get plannerCanvas_turnRule_thirds => 'Tercios'; + @override String get planning_appBar_title => 'Planificacion'; diff --git a/lib/l10n/arb/app_localizations_fr.dart b/lib/l10n/arb/app_localizations_fr.dart index e62307a8d..0ae725f2f 100644 --- a/lib/l10n/arb/app_localizations_fr.dart +++ b/lib/l10n/arb/app_localizations_fr.dart @@ -10452,6 +10452,40 @@ class AppLocalizationsFr extends AppLocalizations { return '$_temp0'; } + @override + String get plannerCanvas_contingency_base => 'Base'; + + @override + String get plannerCanvas_contingency_depthDelta => 'Profondeur en plus'; + + @override + String plannerCanvas_contingency_lostGas(String gas) { + return '$gas perdu'; + } + + @override + String get plannerCanvas_contingency_timeDelta => 'Minutes en plus'; + + @override + String get plannerCanvas_contingency_title => 'Plans de secours'; + + @override + String get plannerCanvas_contingency_turnFraction => 'Fraction de demi-tour'; + + @override + String get plannerCanvas_contingency_turnRule => + 'Règle de pression de demi-tour'; + + @override + String plannerCanvas_gas_minGas(String pressure) { + return 'gaz mini $pressure'; + } + + @override + String plannerCanvas_gas_turnAt(String pressure) { + return 'demi-tour à $pressure'; + } + @override String plannerCanvas_issue_gasDensityCritical(String value) { return 'Densité du gaz $value g/L au-dessus de la limite stricte'; @@ -10467,6 +10501,11 @@ class AppLocalizationsFr extends AppLocalizations { return 'Gaz hypoxique à $depth (ppO₂ $value bar)'; } + @override + String plannerCanvas_issue_minGas(String pressure) { + return 'Le bloc finit sous le minimum rock bottom de $pressure'; + } + @override String get plannerCanvas_issue_noBailout => 'Le plan de décompression CCR n\'emporte aucun gaz de bailout'; @@ -10517,6 +10556,21 @@ class AppLocalizationsFr extends AppLocalizations { @override String get plannerCanvas_table_stop => 'Palier'; + @override + String get plannerCanvas_turnRule_allUsable => 'Tout utilisable'; + + @override + String get plannerCanvas_turnRule_custom => 'Personnalisée'; + + @override + String get plannerCanvas_turnRule_halves => 'Moitiés'; + + @override + String get plannerCanvas_turnRule_none => 'Aucune'; + + @override + String get plannerCanvas_turnRule_thirds => 'Tiers'; + @override String get planning_appBar_title => 'Planification'; diff --git a/lib/l10n/arb/app_localizations_he.dart b/lib/l10n/arb/app_localizations_he.dart index 77cc151a3..bad531a29 100644 --- a/lib/l10n/arb/app_localizations_he.dart +++ b/lib/l10n/arb/app_localizations_he.dart @@ -10151,6 +10151,39 @@ class AppLocalizationsHe extends AppLocalizations { return '$_temp0'; } + @override + String get plannerCanvas_contingency_base => 'בסיס'; + + @override + String get plannerCanvas_contingency_depthDelta => 'עומק נוסף'; + + @override + String plannerCanvas_contingency_lostGas(String gas) { + return 'אבדן $gas'; + } + + @override + String get plannerCanvas_contingency_timeDelta => 'דקות נוספות'; + + @override + String get plannerCanvas_contingency_title => 'תוכניות חירום'; + + @override + String get plannerCanvas_contingency_turnFraction => 'שבר פנייה'; + + @override + String get plannerCanvas_contingency_turnRule => 'כלל לחץ פנייה'; + + @override + String plannerCanvas_gas_minGas(String pressure) { + return 'גז מינימלי $pressure'; + } + + @override + String plannerCanvas_gas_turnAt(String pressure) { + return 'פנייה ב-$pressure'; + } + @override String plannerCanvas_issue_gasDensityCritical(String value) { return 'צפיפות הגז $value g/L מעל הגבול המרבי'; @@ -10166,6 +10199,11 @@ class AppLocalizationsHe extends AppLocalizations { return 'גז היפוקסי ב-$depth (ppO₂ $value bar)'; } + @override + String plannerCanvas_issue_minGas(String pressure) { + return 'המיכל מסתיים מתחת למינימום rock bottom של $pressure'; + } + @override String get plannerCanvas_issue_noBailout => 'תוכנית דקומפרסיה CCR ללא גז חילוץ (bailout)'; @@ -10216,6 +10254,21 @@ class AppLocalizationsHe extends AppLocalizations { @override String get plannerCanvas_table_stop => 'עצירה'; + @override + String get plannerCanvas_turnRule_allUsable => 'הכול שמיש'; + + @override + String get plannerCanvas_turnRule_custom => 'מותאם אישית'; + + @override + String get plannerCanvas_turnRule_halves => 'חצאים'; + + @override + String get plannerCanvas_turnRule_none => 'ללא'; + + @override + String get plannerCanvas_turnRule_thirds => 'שלישים'; + @override String get planning_appBar_title => 'תכנון'; diff --git a/lib/l10n/arb/app_localizations_hu.dart b/lib/l10n/arb/app_localizations_hu.dart index f52fc80d9..42537f560 100644 --- a/lib/l10n/arb/app_localizations_hu.dart +++ b/lib/l10n/arb/app_localizations_hu.dart @@ -10389,6 +10389,39 @@ class AppLocalizationsHu extends AppLocalizations { return '$_temp0'; } + @override + String get plannerCanvas_contingency_base => 'Alap'; + + @override + String get plannerCanvas_contingency_depthDelta => 'Extra mélység'; + + @override + String plannerCanvas_contingency_lostGas(String gas) { + return '$gas elveszett'; + } + + @override + String get plannerCanvas_contingency_timeDelta => 'Extra percek'; + + @override + String get plannerCanvas_contingency_title => 'Vészhelyzeti tervek'; + + @override + String get plannerCanvas_contingency_turnFraction => 'Fordulási hányad'; + + @override + String get plannerCanvas_contingency_turnRule => 'Fordulónyomás-szabály'; + + @override + String plannerCanvas_gas_minGas(String pressure) { + return 'min. gáz $pressure'; + } + + @override + String plannerCanvas_gas_turnAt(String pressure) { + return 'fordulás $pressure-nál'; + } + @override String plannerCanvas_issue_gasDensityCritical(String value) { return 'Gázsűrűség $value g/L a szigorú határ felett'; @@ -10404,6 +10437,11 @@ class AppLocalizationsHu extends AppLocalizations { return 'Hipoxiás gáz $depth mélységben (ppO₂ $value bar)'; } + @override + String plannerCanvas_issue_minGas(String pressure) { + return 'A palack a rock-bottom minimum ($pressure) alatt végez'; + } + @override String get plannerCanvas_issue_noBailout => 'A CCR dekompressziós terv nem tartalmaz bailout gázt'; @@ -10454,6 +10492,21 @@ class AppLocalizationsHu extends AppLocalizations { @override String get plannerCanvas_table_stop => 'Megálló'; + @override + String get plannerCanvas_turnRule_allUsable => 'Minden felhasználható'; + + @override + String get plannerCanvas_turnRule_custom => 'Egyéni'; + + @override + String get plannerCanvas_turnRule_halves => 'Felek'; + + @override + String get plannerCanvas_turnRule_none => 'Nincs'; + + @override + String get plannerCanvas_turnRule_thirds => 'Harmadok'; + @override String get planning_appBar_title => 'Tervezes'; diff --git a/lib/l10n/arb/app_localizations_it.dart b/lib/l10n/arb/app_localizations_it.dart index 3f83869b3..0e48ab48f 100644 --- a/lib/l10n/arb/app_localizations_it.dart +++ b/lib/l10n/arb/app_localizations_it.dart @@ -10417,6 +10417,40 @@ class AppLocalizationsIt extends AppLocalizations { return '$_temp0'; } + @override + String get plannerCanvas_contingency_base => 'Base'; + + @override + String get plannerCanvas_contingency_depthDelta => 'Profondità extra'; + + @override + String plannerCanvas_contingency_lostGas(String gas) { + return '$gas perso'; + } + + @override + String get plannerCanvas_contingency_timeDelta => 'Minuti extra'; + + @override + String get plannerCanvas_contingency_title => 'Contingenze'; + + @override + String get plannerCanvas_contingency_turnFraction => 'Frazione di rientro'; + + @override + String get plannerCanvas_contingency_turnRule => + 'Regola pressione di rientro'; + + @override + String plannerCanvas_gas_minGas(String pressure) { + return 'gas minimo $pressure'; + } + + @override + String plannerCanvas_gas_turnAt(String pressure) { + return 'rientro a $pressure'; + } + @override String plannerCanvas_issue_gasDensityCritical(String value) { return 'Densità del gas $value g/L oltre il limite massimo'; @@ -10432,6 +10466,11 @@ class AppLocalizationsIt extends AppLocalizations { return 'Gas ipossico a $depth (ppO₂ $value bar)'; } + @override + String plannerCanvas_issue_minGas(String pressure) { + return 'La bombola termina sotto il minimo rock bottom di $pressure'; + } + @override String get plannerCanvas_issue_noBailout => 'Il piano di decompressione CCR non prevede gas di bailout'; @@ -10483,6 +10522,21 @@ class AppLocalizationsIt extends AppLocalizations { @override String get plannerCanvas_table_stop => 'Sosta'; + @override + String get plannerCanvas_turnRule_allUsable => 'Tutto utilizzabile'; + + @override + String get plannerCanvas_turnRule_custom => 'Personalizzata'; + + @override + String get plannerCanvas_turnRule_halves => 'Metà'; + + @override + String get plannerCanvas_turnRule_none => 'Nessuna'; + + @override + String get plannerCanvas_turnRule_thirds => 'Terzi'; + @override String get planning_appBar_title => 'Pianificazione'; diff --git a/lib/l10n/arb/app_localizations_nl.dart b/lib/l10n/arb/app_localizations_nl.dart index 21368b612..43c7733f8 100644 --- a/lib/l10n/arb/app_localizations_nl.dart +++ b/lib/l10n/arb/app_localizations_nl.dart @@ -10340,6 +10340,39 @@ class AppLocalizationsNl extends AppLocalizations { return '$_temp0'; } + @override + String get plannerCanvas_contingency_base => 'Basis'; + + @override + String get plannerCanvas_contingency_depthDelta => 'Extra diepte'; + + @override + String plannerCanvas_contingency_lostGas(String gas) { + return '$gas verloren'; + } + + @override + String get plannerCanvas_contingency_timeDelta => 'Extra minuten'; + + @override + String get plannerCanvas_contingency_title => 'Noodplannen'; + + @override + String get plannerCanvas_contingency_turnFraction => 'Keerfractie'; + + @override + String get plannerCanvas_contingency_turnRule => 'Keerdruk-regel'; + + @override + String plannerCanvas_gas_minGas(String pressure) { + return 'min. gas $pressure'; + } + + @override + String plannerCanvas_gas_turnAt(String pressure) { + return 'keer om bij $pressure'; + } + @override String plannerCanvas_issue_gasDensityCritical(String value) { return 'Gasdichtheid $value g/L boven harde limiet'; @@ -10355,6 +10388,11 @@ class AppLocalizationsNl extends AppLocalizations { return 'Hypoxisch gas op $depth (ppO₂ $value bar)'; } + @override + String plannerCanvas_issue_minGas(String pressure) { + return 'Fles eindigt onder het rock-bottom-minimum van $pressure'; + } + @override String get plannerCanvas_issue_noBailout => 'CCR-decompressieplan heeft geen bailoutgas aan boord'; @@ -10405,6 +10443,21 @@ class AppLocalizationsNl extends AppLocalizations { @override String get plannerCanvas_table_stop => 'Stop'; + @override + String get plannerCanvas_turnRule_allUsable => 'Alles bruikbaar'; + + @override + String get plannerCanvas_turnRule_custom => 'Aangepast'; + + @override + String get plannerCanvas_turnRule_halves => 'Helften'; + + @override + String get plannerCanvas_turnRule_none => 'Geen'; + + @override + String get plannerCanvas_turnRule_thirds => 'Derden'; + @override String get planning_appBar_title => 'Planning'; diff --git a/lib/l10n/arb/app_localizations_pt.dart b/lib/l10n/arb/app_localizations_pt.dart index f98b244e7..77507f791 100644 --- a/lib/l10n/arb/app_localizations_pt.dart +++ b/lib/l10n/arb/app_localizations_pt.dart @@ -10423,6 +10423,40 @@ class AppLocalizationsPt extends AppLocalizations { return '$_temp0'; } + @override + String get plannerCanvas_contingency_base => 'Base'; + + @override + String get plannerCanvas_contingency_depthDelta => 'Profundidade extra'; + + @override + String plannerCanvas_contingency_lostGas(String gas) { + return '$gas perdido'; + } + + @override + String get plannerCanvas_contingency_timeDelta => 'Minutos extras'; + + @override + String get plannerCanvas_contingency_title => 'Contingências'; + + @override + String get plannerCanvas_contingency_turnFraction => 'Fração de retorno'; + + @override + String get plannerCanvas_contingency_turnRule => + 'Regra de pressão de retorno'; + + @override + String plannerCanvas_gas_minGas(String pressure) { + return 'gás mínimo $pressure'; + } + + @override + String plannerCanvas_gas_turnAt(String pressure) { + return 'retorno a $pressure'; + } + @override String plannerCanvas_issue_gasDensityCritical(String value) { return 'Densidade do gás $value g/L acima do limite máximo'; @@ -10438,6 +10472,11 @@ class AppLocalizationsPt extends AppLocalizations { return 'Gás hipóxico a $depth (ppO₂ $value bar)'; } + @override + String plannerCanvas_issue_minGas(String pressure) { + return 'O cilindro termina abaixo do mínimo rock bottom de $pressure'; + } + @override String get plannerCanvas_issue_noBailout => 'O plano de descompressão CCR não leva gás de bailout'; @@ -10488,6 +10527,21 @@ class AppLocalizationsPt extends AppLocalizations { @override String get plannerCanvas_table_stop => 'Parada'; + @override + String get plannerCanvas_turnRule_allUsable => 'Tudo utilizável'; + + @override + String get plannerCanvas_turnRule_custom => 'Personalizada'; + + @override + String get plannerCanvas_turnRule_halves => 'Metades'; + + @override + String get plannerCanvas_turnRule_none => 'Nenhuma'; + + @override + String get plannerCanvas_turnRule_thirds => 'Terços'; + @override String get planning_appBar_title => 'Planejamento'; diff --git a/lib/l10n/arb/app_localizations_zh.dart b/lib/l10n/arb/app_localizations_zh.dart index bf1efc528..9d34c4122 100644 --- a/lib/l10n/arb/app_localizations_zh.dart +++ b/lib/l10n/arb/app_localizations_zh.dart @@ -9965,6 +9965,39 @@ class AppLocalizationsZh extends AppLocalizations { return '$_temp0'; } + @override + String get plannerCanvas_contingency_base => '基准'; + + @override + String get plannerCanvas_contingency_depthDelta => '额外深度'; + + @override + String plannerCanvas_contingency_lostGas(String gas) { + return '失去 $gas'; + } + + @override + String get plannerCanvas_contingency_timeDelta => '额外分钟'; + + @override + String get plannerCanvas_contingency_title => '应急计划'; + + @override + String get plannerCanvas_contingency_turnFraction => '折返比例'; + + @override + String get plannerCanvas_contingency_turnRule => '折返压力规则'; + + @override + String plannerCanvas_gas_minGas(String pressure) { + return '最低气量 $pressure'; + } + + @override + String plannerCanvas_gas_turnAt(String pressure) { + return '$pressure 时折返'; + } + @override String plannerCanvas_issue_gasDensityCritical(String value) { return '气体密度 $value g/L 超过硬性上限'; @@ -9980,6 +10013,11 @@ class AppLocalizationsZh extends AppLocalizations { return '$depth 处为低氧气体(ppO₂ $value bar)'; } + @override + String plannerCanvas_issue_minGas(String pressure) { + return '气瓶终压低于最低保底气量 $pressure'; + } + @override String get plannerCanvas_issue_noBailout => 'CCR 减压计划未携带逃生气体'; @@ -10028,6 +10066,21 @@ class AppLocalizationsZh extends AppLocalizations { @override String get plannerCanvas_table_stop => '停留'; + @override + String get plannerCanvas_turnRule_allUsable => '全部可用'; + + @override + String get plannerCanvas_turnRule_custom => '自定义'; + + @override + String get plannerCanvas_turnRule_halves => '对半'; + + @override + String get plannerCanvas_turnRule_none => '无'; + + @override + String get plannerCanvas_turnRule_thirds => '三分之一'; + @override String get planning_appBar_title => '计划'; diff --git a/lib/l10n/arb/app_nl.arb b/lib/l10n/arb/app_nl.arb index f5672dbb6..677cb1c47 100644 --- a/lib/l10n/arb/app_nl.arb +++ b/lib/l10n/arb/app_nl.arb @@ -3247,9 +3247,19 @@ "plannerCanvas_ccr_switchDepth": "Setpoint-wisseldiepte", "plannerCanvas_chip_cns": "CNS {value}%", "plannerCanvas_chip_issues": "{count, plural, =1{1 probleem} other{{count} problemen}}", + "plannerCanvas_contingency_base": "Basis", + "plannerCanvas_contingency_depthDelta": "Extra diepte", + "plannerCanvas_contingency_lostGas": "{gas} verloren", + "plannerCanvas_contingency_timeDelta": "Extra minuten", + "plannerCanvas_contingency_title": "Noodplannen", + "plannerCanvas_contingency_turnFraction": "Keerfractie", + "plannerCanvas_contingency_turnRule": "Keerdruk-regel", + "plannerCanvas_gas_minGas": "min. gas {pressure}", + "plannerCanvas_gas_turnAt": "keer om bij {pressure}", "plannerCanvas_issue_gasDensityCritical": "Gasdichtheid {value} g/L boven harde limiet", "plannerCanvas_issue_gasDensityHigh": "Gasdichtheid {value} g/L boven aanbevolen limiet", "plannerCanvas_issue_hypoxic": "Hypoxisch gas op {depth} (ppO₂ {value} bar)", + "plannerCanvas_issue_minGas": "Fles eindigt onder het rock-bottom-minimum van {pressure}", "plannerCanvas_issue_noBailout": "CCR-decompressieplan heeft geen bailoutgas aan boord", "plannerCanvas_issue_noDecoGas": "Decompressie vereist maar geen decogas meegenomen", "plannerCanvas_results_noDeco": "Geen decompressie vereist", @@ -3264,6 +3274,11 @@ "plannerCanvas_table_gas": "Gas", "plannerCanvas_table_runtime": "RT", "plannerCanvas_table_stop": "Stop", + "plannerCanvas_turnRule_allUsable": "Alles bruikbaar", + "plannerCanvas_turnRule_custom": "Aangepast", + "plannerCanvas_turnRule_halves": "Helften", + "plannerCanvas_turnRule_none": "Geen", + "plannerCanvas_turnRule_thirds": "Derden", "planning_appBar_title": "Planning", "planning_card_decoCalculator_description": "Bereken no-decompressielimieten, benodigde decostops en CNS/OTU-blootstelling voor duikprofielen met meerdere niveaus.", "planning_card_decoCalculator_subtitle": "Plan duiken met decompressiestops", diff --git a/lib/l10n/arb/app_pt.arb b/lib/l10n/arb/app_pt.arb index 9058503ee..b35892887 100644 --- a/lib/l10n/arb/app_pt.arb +++ b/lib/l10n/arb/app_pt.arb @@ -3247,9 +3247,19 @@ "plannerCanvas_ccr_switchDepth": "Profundidade de troca do setpoint", "plannerCanvas_chip_cns": "CNS {value}%", "plannerCanvas_chip_issues": "{count, plural, =1{1 problema} other{{count} problemas}}", + "plannerCanvas_contingency_base": "Base", + "plannerCanvas_contingency_depthDelta": "Profundidade extra", + "plannerCanvas_contingency_lostGas": "{gas} perdido", + "plannerCanvas_contingency_timeDelta": "Minutos extras", + "plannerCanvas_contingency_title": "Contingências", + "plannerCanvas_contingency_turnFraction": "Fração de retorno", + "plannerCanvas_contingency_turnRule": "Regra de pressão de retorno", + "plannerCanvas_gas_minGas": "gás mínimo {pressure}", + "plannerCanvas_gas_turnAt": "retorno a {pressure}", "plannerCanvas_issue_gasDensityCritical": "Densidade do gás {value} g/L acima do limite máximo", "plannerCanvas_issue_gasDensityHigh": "Densidade do gás {value} g/L acima do limite recomendado", "plannerCanvas_issue_hypoxic": "Gás hipóxico a {depth} (ppO₂ {value} bar)", + "plannerCanvas_issue_minGas": "O cilindro termina abaixo do mínimo rock bottom de {pressure}", "plannerCanvas_issue_noBailout": "O plano de descompressão CCR não leva gás de bailout", "plannerCanvas_issue_noDecoGas": "Descompressão necessária mas sem gás de deco a bordo", "plannerCanvas_results_noDeco": "Nenhuma descompressão necessária", @@ -3264,6 +3274,11 @@ "plannerCanvas_table_gas": "Gás", "plannerCanvas_table_runtime": "RT", "plannerCanvas_table_stop": "Parada", + "plannerCanvas_turnRule_allUsable": "Tudo utilizável", + "plannerCanvas_turnRule_custom": "Personalizada", + "plannerCanvas_turnRule_halves": "Metades", + "plannerCanvas_turnRule_none": "Nenhuma", + "plannerCanvas_turnRule_thirds": "Terços", "planning_appBar_title": "Planejamento", "planning_card_decoCalculator_description": "Calcule limites de nao descompressao, paradas deco necessarias e exposicao CNS/OTU para perfis de mergulho multinivel.", "planning_card_decoCalculator_subtitle": "Planeje mergulhos com paradas de descompressao", diff --git a/lib/l10n/arb/app_zh.arb b/lib/l10n/arb/app_zh.arb index b14880db0..9cbe62832 100644 --- a/lib/l10n/arb/app_zh.arb +++ b/lib/l10n/arb/app_zh.arb @@ -3397,9 +3397,19 @@ "plannerCanvas_ccr_switchDepth": "设定点切换深度", "plannerCanvas_chip_cns": "CNS {value}%", "plannerCanvas_chip_issues": "{count, plural, other{{count} 个问题}}", + "plannerCanvas_contingency_base": "基准", + "plannerCanvas_contingency_depthDelta": "额外深度", + "plannerCanvas_contingency_lostGas": "失去 {gas}", + "plannerCanvas_contingency_timeDelta": "额外分钟", + "plannerCanvas_contingency_title": "应急计划", + "plannerCanvas_contingency_turnFraction": "折返比例", + "plannerCanvas_contingency_turnRule": "折返压力规则", + "plannerCanvas_gas_minGas": "最低气量 {pressure}", + "plannerCanvas_gas_turnAt": "{pressure} 时折返", "plannerCanvas_issue_gasDensityCritical": "气体密度 {value} g/L 超过硬性上限", "plannerCanvas_issue_gasDensityHigh": "气体密度 {value} g/L 超过建议上限", "plannerCanvas_issue_hypoxic": "{depth} 处为低氧气体(ppO₂ {value} bar)", + "plannerCanvas_issue_minGas": "气瓶终压低于最低保底气量 {pressure}", "plannerCanvas_issue_noBailout": "CCR 减压计划未携带逃生气体", "plannerCanvas_issue_noDecoGas": "需要减压但未携带减压气体", "plannerCanvas_results_noDeco": "无需减压", @@ -3414,6 +3424,11 @@ "plannerCanvas_table_gas": "气体", "plannerCanvas_table_runtime": "RT", "plannerCanvas_table_stop": "停留", + "plannerCanvas_turnRule_allUsable": "全部可用", + "plannerCanvas_turnRule_custom": "自定义", + "plannerCanvas_turnRule_halves": "对半", + "plannerCanvas_turnRule_none": "无", + "plannerCanvas_turnRule_thirds": "三分之一", "planning_appBar_title": "计划", "planning_card_decoCalculator_description": "计算免减压极限、所需减压停留以及多层潜水轮廓的中枢神经系统毒性/氧毒性单位暴露量。", "planning_card_decoCalculator_subtitle": "规划需要减压停留的潜水", diff --git a/test/features/planner/contingency_service_test.dart b/test/features/planner/contingency_service_test.dart new file mode 100644 index 000000000..a68252d2d --- /dev/null +++ b/test/features/planner/contingency_service_test.dart @@ -0,0 +1,181 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:submersion/core/constants/enums.dart'; +import 'package:submersion/features/dive_log/domain/entities/dive.dart'; +import 'package:submersion/features/dive_planner/domain/entities/plan_segment.dart'; +import 'package:submersion/features/planner/domain/entities/dive_plan.dart' + as domain; +import 'package:submersion/features/planner/domain/entities/plan_outcome.dart'; +import 'package:submersion/features/planner/domain/services/contingency_service.dart'; +import 'package:submersion/features/planner/domain/services/plan_engine.dart'; + +const _backGas = GasMix(o2: 18, he: 45); +const _backTank = DiveTank( + id: 'back', + volume: 24, + startPressure: 232, + gasMix: _backGas, +); +const _ean50 = DiveTank( + id: 'ean50', + volume: 11.1, + startPressure: 207, + gasMix: GasMix(o2: 50), + role: TankRole.deco, +); + +domain.DivePlan _plan({ + domain.PlanMode mode = domain.PlanMode.oc, + List tanks = const [_backTank, _ean50], +}) { + return domain.DivePlan( + id: 'plan-1', + name: 'Contingency test', + mode: mode, + gfLow: 50, + gfHigh: 80, + deviationDepthDelta: 5.0, + deviationTimeMinutes: 5, + tanks: tanks, + segments: [ + PlanSegment.descent( + id: 'seg-1', + targetDepth: 60.0, + tankId: 'back', + gasMix: _backGas, + order: 0, + ), + PlanSegment.bottom( + id: 'seg-2', + depth: 60.0, + durationMinutes: 25, + tankId: 'back', + gasMix: _backGas, + order: 1, + ), + ], + createdAt: DateTime(2026, 7, 5), + updatedAt: DateTime(2026, 7, 5), + ); +} + +void main() { + const service = ContingencyService(); + const engine = PlanEngine(); + + test('deviations produce deeper, longer, and combined variants', () { + final base = engine.compute(_plan()); + final deviations = service.deviations(_plan()); + + expect(deviations.map((d) => d.key), ['deeper', 'longer', 'both']); + final deeper = deviations[0]; + final longer = deviations[1]; + final both = deviations[2]; + + expect(deeper.outcome.maxDepth, closeTo(65.0, 0.01)); + expect(deeper.outcome.totalDecoSeconds, greaterThan(base.totalDecoSeconds)); + expect(longer.outcome.maxDepth, closeTo(60.0, 0.01)); + expect(longer.outcome.totalDecoSeconds, greaterThan(base.totalDecoSeconds)); + expect( + both.outcome.totalDecoSeconds, + greaterThan(deeper.outcome.totalDecoSeconds), + ); + expect( + both.outcome.totalDecoSeconds, + greaterThan(longer.outcome.totalDecoSeconds), + ); + }); + + test('lost EAN50 lengthens deco and removes the gas from the stops', () { + final base = engine.compute(_plan()); + final lost = service.lostGas(_plan()); + + expect(lost, hasLength(1)); + expect(lost.single.tank.id, 'ean50'); + final outcome = lost.single.outcome; + expect(outcome.totalDecoSeconds, greaterThan(base.totalDecoSeconds)); + for (final stop in outcome.stops.where((s) => s.depthMeters <= 22.0)) { + expect(stop.gasFO2, isNot(closeTo(0.50, 0.01))); + } + }); + + test('CCR plans yield no lost-gas outcomes', () { + expect(service.lostGas(_plan(mode: domain.PlanMode.ccr)), isEmpty); + }); + + test('deviationFor runs a single named variant', () { + final deeper = service.deviationFor(_plan(), 'deeper'); + expect(deeper, isNotNull); + expect(deeper!.key, 'deeper'); + expect(deeper.outcome.maxDepth, closeTo(65.0, 0.01)); + expect(service.deviationFor(_plan(), 'longer')!.key, 'longer'); + // Empty plan yields nothing to vary. + final empty = domain.DivePlan( + id: 'e', + name: 'e', + gfLow: 50, + gfHigh: 80, + createdAt: DateTime(2026, 7, 5), + updatedAt: DateTime(2026, 7, 5), + ); + expect(service.deviationFor(empty, 'deeper'), isNull); + }); + + test('losing a gas remaps user segments that breathed it onto back gas', () { + // A (contrived) plan whose bottom segment breathes the EAN50 deco tank. + // Losing EAN50 must remap that segment to back gas, so the contingency + // does not keep breathing 50% at 60 m (which would be ppO2-critical). + final plan = domain.DivePlan( + id: 'plan-x', + name: 'Segment on deco gas', + gfLow: 50, + gfHigh: 80, + tanks: const [_backTank, _ean50], + segments: [ + PlanSegment.descent( + id: 'seg-1', + targetDepth: 60.0, + tankId: 'back', + gasMix: _backGas, + order: 0, + ), + PlanSegment.bottom( + id: 'seg-2', + depth: 60.0, + durationMinutes: 20, + tankId: 'ean50', + gasMix: const GasMix(o2: 50), + order: 1, + ), + ], + createdAt: DateTime(2026, 7, 5), + updatedAt: DateTime(2026, 7, 5), + ); + + final lost = service.lostGas(plan); + expect(lost, hasLength(1)); + final outcome = lost.single.outcome; + // Back gas (18/45) at 60 m is safe; EAN50 at 60 m would be ppO2-critical. + expect( + outcome.issues.map((i) => i.type), + isNot(contains(PlanIssueType.ppO2Critical)), + reason: 'remapped segment should breathe back gas, not the lost EAN50', + ); + }); + + test('losing the only cylinder yields no lost-gas outcome', () { + final plan = _plan(tanks: const [_ean50]); + expect(service.lostGas(plan), isEmpty); + }); + + test('no segments yields no deviations', () { + final empty = domain.DivePlan( + id: 'p', + name: 'empty', + gfLow: 50, + gfHigh: 80, + createdAt: DateTime(2026, 7, 5), + updatedAt: DateTime(2026, 7, 5), + ); + expect(service.deviations(empty), isEmpty); + }); +} diff --git a/test/features/planner/contingency_ui_test.dart b/test/features/planner/contingency_ui_test.dart new file mode 100644 index 000000000..681ea6911 --- /dev/null +++ b/test/features/planner/contingency_ui_test.dart @@ -0,0 +1,168 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:submersion/core/constants/map_style.dart'; +import 'package:submersion/core/providers/provider.dart'; +import 'package:submersion/features/dive_planner/presentation/providers/dive_planner_providers.dart'; +import 'package:submersion/features/planner/domain/entities/dive_plan.dart' + as domain; +import 'package:submersion/features/planner/presentation/providers/plan_canvas_providers.dart'; +import 'package:submersion/features/planner/presentation/widgets/contingency_chips.dart'; +import 'package:submersion/features/planner/presentation/widgets/contingency_settings_section.dart'; +import 'package:submersion/features/planner/presentation/widgets/plan_canvas_chart.dart'; +import 'package:submersion/features/planner/presentation/widgets/plan_results_sheet.dart'; +import 'package:submersion/features/settings/presentation/providers/settings_providers.dart'; + +import '../../helpers/test_app.dart'; + +class _TestSettingsNotifier extends StateNotifier + implements SettingsNotifier { + _TestSettingsNotifier() : super(const AppSettings()); + + @override + Future setMapStyle(MapStyle style) async => + state = state.copyWith(mapStyle: style); + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +List _overrides() => [ + settingsProvider.overrideWith((ref) => _TestSettingsNotifier()), +]; + +void main() { + testWidgets('selecting a deviation ghosts a second profile line', ( + tester, + ) async { + await tester.pumpWidget( + testApp( + overrides: _overrides(), + child: const SizedBox( + width: 500, + height: 400, + child: Column( + children: [ + Expanded(child: PlanCanvasChart()), + ContingencyChips(), + ], + ), + ), + ), + ); + final container = ProviderScope.containerOf( + tester.element(find.byType(PlanCanvasChart)), + ); + container + .read(divePlanNotifierProvider.notifier) + .addSimplePlan(maxDepth: 45, bottomTimeMinutes: 25); + await tester.pumpAndSettle(); + + LineChart chart() => tester.widget(find.byType(LineChart)); + final baseBars = chart().data.lineBarsData.length; + + await tester.tap(find.text('+5m')); + await tester.pumpAndSettle(); + + expect(container.read(selectedDeviationProvider), 'deeper'); + expect(chart().data.lineBarsData.length, baseBars + 1); + + // Back to base clears the ghost. + await tester.tap(find.text('Base')); + await tester.pumpAndSettle(); + expect(chart().data.lineBarsData.length, baseBars); + }); + + testWidgets('sheet lists deviation tables and turn pressure', (tester) async { + await tester.pumpWidget( + testApp( + overrides: _overrides(), + child: SizedBox( + width: 520, + height: 900, + child: PlanResultsSheet(controller: ScrollController()), + ), + ), + ); + final container = ProviderScope.containerOf( + tester.element(find.byType(PlanResultsSheet)), + ); + final notifier = container.read(divePlanNotifierProvider.notifier); + notifier.addSimplePlan(maxDepth: 45, bottomTimeMinutes: 25); + notifier.updateContingencies(turnRule: domain.TurnPressureRule.thirds); + await tester.pumpAndSettle(); + + expect(find.textContaining('turn @'), findsOneWidget); + await tester.scrollUntilVisible( + find.text('CONTINGENCIES'), + 200, + scrollable: find.byType(Scrollable).first, + ); + expect(find.text('CONTINGENCIES'), findsOneWidget); + + // The deviation tables are collapsed by default (they cost extra engine + // runs); expand the section before the sub-tables render. + expect(find.text('+5′'), findsNothing); + expect(container.read(contingenciesExpandedProvider), isFalse); + await tester.tap(find.text('CONTINGENCIES')); + await tester.pumpAndSettle(); + expect(container.read(contingenciesExpandedProvider), isTrue); + + await tester.scrollUntilVisible( + find.text('+5′'), + 200, + scrollable: find.byType(Scrollable).first, + ); + expect(find.text('+5′'), findsOneWidget); + }); + + testWidgets('settings section edits deltas, turn rule, and custom fraction', ( + tester, + ) async { + await tester.pumpWidget( + testApp( + overrides: _overrides(), + child: const SizedBox( + width: 500, + child: SingleChildScrollView(child: ContingencySettingsSection()), + ), + ), + ); + await tester.pumpAndSettle(); + final container = ProviderScope.containerOf( + tester.element(find.byType(ContingencySettingsSection)), + ); + + final fields = find.byType(TextFormField); + // Depth delta and time delta fields (fraction appears only for custom). + await tester.enterText(fields.at(0), '8'); + await tester.enterText(fields.at(1), '12'); + await tester.pumpAndSettle(); + expect(container.read(divePlanNotifierProvider).deviationDepthDelta, 8.0); + expect(container.read(divePlanNotifierProvider).deviationTimeMinutes, 12); + + // Pick a turn-pressure rule from the dropdown. + await tester.tap( + find.byType(DropdownButtonFormField), + ); + await tester.pumpAndSettle(); + await tester.tap(find.text('Thirds').last); + await tester.pumpAndSettle(); + expect( + container.read(divePlanNotifierProvider).turnPressureRule, + domain.TurnPressureRule.thirds, + ); + + // Custom exposes a fraction field. + await tester.tap( + find.byType(DropdownButtonFormField), + ); + await tester.pumpAndSettle(); + await tester.tap(find.text('Custom').last); + await tester.pumpAndSettle(); + final fractionField = find.byType(TextFormField).last; + await tester.enterText(fractionField, '0.4'); + await tester.pumpAndSettle(); + expect(container.read(divePlanNotifierProvider).turnPressureFraction, 0.4); + }); +} diff --git a/test/features/planner/dive_plan_state_mapper_test.dart b/test/features/planner/dive_plan_state_mapper_test.dart index 4a3c2f475..9639bfad0 100644 --- a/test/features/planner/dive_plan_state_mapper_test.dart +++ b/test/features/planner/dive_plan_state_mapper_test.dart @@ -78,17 +78,18 @@ void main() { final merged = divePlanFromState(state(), existing: existing); // State-owned fields come from the state (mode/setpoints joined that - // set in Phase 4: the state defaults to OC with null setpoints)... + // set in Phase 4, contingency config in Phase 5: the state defaults to + // OC / null setpoints / no turn rule)... expect(merged.name, 'Reef dive'); expect(merged.gfLow, 45); expect(merged.sacBottom, 17.0); expect(merged.mode, domain.PlanMode.oc); expect(merged.setpointHigh, isNull); + expect(merged.turnPressureRule, isNull); // ...while aggregate-only fields survive the cycle. expect(merged.waterType, WaterType.fresh); expect(merged.descentRate, 20.0); expect(merged.airBreaks, isNotNull); - expect(merged.turnPressureRule, domain.TurnPressureRule.thirds); expect(merged.sourceDiveId, 'dive-9'); }); @@ -133,4 +134,22 @@ void main() { expect(merged.siteId, isNull); expect(merged.surfaceInterval, isNull); }); + + test('contingency config round-trips through the state', () { + final configured = state().copyWith( + deviationDepthDelta: 6.0, + deviationTimeMinutes: 10, + turnPressureRule: domain.TurnPressureRule.halves, + turnPressureFraction: 0.4, + ); + final plan = divePlanFromState(configured); + expect(plan.deviationDepthDelta, 6.0); + expect(plan.deviationTimeMinutes, 10); + expect(plan.turnPressureRule, domain.TurnPressureRule.halves); + expect(plan.turnPressureFraction, 0.4); + + final restored = stateFromDivePlan(plan); + expect(restored.deviationDepthDelta, 6.0); + expect(restored.turnPressureRule, domain.TurnPressureRule.halves); + }); } diff --git a/test/features/planner/plan_engine_issues_test.dart b/test/features/planner/plan_engine_issues_test.dart index e60c58e3c..c7a8bdeda 100644 --- a/test/features/planner/plan_engine_issues_test.dart +++ b/test/features/planner/plan_engine_issues_test.dart @@ -256,6 +256,73 @@ void main() { ); }); }); + + group('PlanEngine turn pressure and rock-bottom', () { + test('thirds rule turns at start minus a third of usable', () { + final plan = _airPlan(depth: 30.0, minutes: 10).copyWith( + turnPressureRule: domain.TurnPressureRule.thirds, + reservePressure: 50.0, + tanks: const [ + DiveTank( + id: 'tank-1', + volume: 24.0, + startPressure: 200, + gasMix: _air, + ), + ], + ); + final outcome = engine.compute(plan); + // usable = 150; turn = 200 - 50 = 150. + expect(outcome.tankUsages.single.turnPressureBar, closeTo(150.0, 1e-9)); + }); + + test('no rule, no turn pressure', () { + final outcome = engine.compute(_airPlan(depth: 30.0, minutes: 10)); + expect(outcome.tankUsages.single.turnPressureBar, isNull); + }); + + test('rock-bottom violation fires when the tank survives but short', () { + // AL80 at 40 m for 18 min ends with gas remaining, but below the + // ~124 bar two-diver stressed rock bottom for that depth. + final outcome = engine.compute( + _airPlan( + depth: 40.0, + minutes: 18, + tanks: const [ + DiveTank( + id: 'tank-1', + volume: 11.1, + startPressure: 207, + gasMix: _air, + ), + ], + ), + ); + final usage = outcome.tankUsages.single; + expect(usage.minGasBar, isNotNull); + expect(usage.remainingPressure!, greaterThan(0)); + expect(usage.remainingPressure!, lessThan(usage.minGasBar!)); + expect(_types(outcome), contains(PlanIssueType.minGasViolation)); + }); + + test('rock-bottom satisfied on a big tank', () { + final outcome = engine.compute( + _airPlan( + depth: 30.0, + minutes: 10, + tanks: const [ + DiveTank( + id: 'tank-1', + volume: 24.0, + startPressure: 232, + gasMix: _air, + ), + ], + ), + ); + expect(_types(outcome), isNot(contains(PlanIssueType.minGasViolation))); + }); + }); } /// A tissue state the Buhlmann engine cannot consume — used to prove the diff --git a/test/features/planner/plan_outcome_entity_test.dart b/test/features/planner/plan_outcome_entity_test.dart index 749dc8803..d93132fd0 100644 --- a/test/features/planner/plan_outcome_entity_test.dart +++ b/test/features/planner/plan_outcome_entity_test.dart @@ -99,7 +99,9 @@ void main() { ); expect(usage.remainingPressure, isNull); expect(usage.reserveViolation, isFalse); - expect(usage.props, hasLength(5)); + expect(usage.turnPressureBar, isNull); + expect(usage.minGasBar, isNull); + expect(usage.props, hasLength(7)); }); }); diff --git a/test/features/planner/plan_results_widgets_test.dart b/test/features/planner/plan_results_widgets_test.dart index 2e96af219..099c8aa92 100644 --- a/test/features/planner/plan_results_widgets_test.dart +++ b/test/features/planner/plan_results_widgets_test.dart @@ -53,11 +53,20 @@ void main() { ); await _seedDecoPlan(tester, find.byType(PlanResultsSheet)); - // Runtime table header. - expect(find.text('Depth'), findsOneWidget); + // Runtime table header(s) — the contingency mini-tables repeat it. + expect(find.text('Depth'), findsWidgets); // Gas section rendered a per-tank consumption bar. expect(find.byType(LinearProgressIndicator), findsWidgets); - // Air at 45 m trips the critical gas-density issue. + // Air at 45 m trips the critical gas-density issue (issues sit below + // the contingency tables — scroll the unique section header into the + // lazy viewport, then nudge so the issue rows build). + await tester.scrollUntilVisible( + find.text('WARNINGS'), + 300, + scrollable: find.byType(Scrollable).first, + ); + await tester.drag(find.byType(Scrollable).first, const Offset(0, -200)); + await tester.pumpAndSettle(); expect(find.textContaining('g/L'), findsWidgets); }); }