From 25f31b299bdae83cc4032344916dac9edc5f8d62 Mon Sep 17 00:00:00 2001 From: Alon Dotan Date: Mon, 14 Sep 2026 20:10:04 +0300 Subject: [PATCH 1/6] feat(import): keep the tissue loading the dive computer recorded (#1977) Dive computers export their own tissue state and the app threw all of it away, then showed a Buhlmann recompute that could never match the numbers the diver saw underwater. Garmin FIT records an aggregate N2 load percent on every sample and start/end N2 in the dive summary, Shearwater Cloud databases record GF99 per sample plus start surface GF and end GF99, Shearwater and Subsurface UDDF carry a per-waypoint gradientfactor, Suunto SML headers (also delivered by Suunto Cloud) carry per-compartment N2 and He tensions at dive start and end with the RGBM factors, and Ratio iX3M XML carries 16 compartment loading percentages on every sample. Two per-sample fields, gf99 and n2_load, are added under a new codec v2 field table. A v1 blob still decodes with them null, and a peer on the old codec stores a v2 blob and skips it until updated, which the existing forward-version handling already covered, so the sync floor is unchanged. Dive-level start and end state lands in a new JSON column, computer_tissue_json (schema 219), as a ComputerTissueSnapshot holding whatever the source had: compartment tensions in bar or loading percent, aggregate N2 load, GF99, surface GF, CNS, OTU, RGBM factors and the algorithm name. Suunto tensions are converted from Pascal; Shearwater's decoModel integer code is spelled out; a zero EndGF99 in dive_details is treated as the placeholder it is and the calculated_values JSON wins. Where the computer reported a value it is now the one shown. The profile GF99 curve is taken from the computer's samples when any exist, falling back to the calculated curve otherwise; this is hardcoded computer-first because every per-metric source toggle is its own settings column and migration. The tissue card gains a dive computer section that renders only when a snapshot exists, with the calculated figure as a secondary line where both are known. Nothing imported carries per-sample per-compartment tensions except Ratio, whose series is not stored yet, so the 16-compartment recompute stays the source of the tissue curves and the 3D scene. Both UDDF writers now emit gradientfactor so a backup round trip keeps the recorded value. Signed-off-by: Alon Dotan --- ...5-imported-profile-gap-priority-tracker.md | 1 + lib/core/constants/profile_metrics.dart | 1 + lib/core/database/database.dart | 38 +- lib/core/database/legacy_sample_staging.dart | 8 +- .../export/uddf/uddf_export_builders.dart | 7 + .../export/uddf/uddf_export_service.dart | 9 + .../export/uddf/uddf_full_import_service.dart | 11 + .../export/uddf/uddf_gradient_factor.dart | 21 + .../export/uddf/uddf_import_service.dart | 9 + .../suunto_cloud/suunto_dive_parser.dart | 9 + .../suunto_cloud/suunto_sml_normalizer.dart | 15 + .../suunto_cloud/suunto_tissue_parser.dart | 80 ++++ .../dive_3d/application/tissue_providers.dart | 5 + .../data/services/dive_import_service.dart | 2 + .../domain/entities/downloaded_dive.dart | 7 + .../data/services/dive_reimport_service.dart | 2 + .../services/fit/fit_profile_extractor.dart | 16 +- .../services/fit/fit_summary_extractor.dart | 6 + .../services/fit/fit_tissue_snapshot.dart | 31 ++ .../data/services/fit_parser_service.dart | 3 + .../data/services/uddf_entity_importer.dart | 9 + .../domain/entities/imported_dive.dart | 22 + .../services/imported_dive_converter.dart | 3 + .../dive_computer_repository_impl.dart | 19 + .../repositories/dive_repository_impl.dart | 5 + .../services/profile_analysis_service.dart | 10 + .../domain/codecs/profile_field_table.dart | 11 + .../domain/codecs/profile_sample.dart | 51 ++- .../domain/codecs/profile_sample_point.dart | 4 + .../domain/codecs/profile_series_codec.dart | 28 +- .../entities/computer_tissue_snapshot.dart | 240 +++++++++++ .../dive_log/domain/entities/dive.dart | 20 + .../domain/services/dive_merge_builder.dart | 1 + .../presentation/pages/dive_detail_page.dart | 2 + .../presentation/pages/dive_edit_page.dart | 1 + .../providers/computer_gf99_overlay.dart | 44 ++ .../providers/profile_analysis_provider.dart | 41 +- .../widgets/compact_tissue_loading_card.dart | 35 ++ .../widgets/computer_tissue_formatter.dart | 69 +++ .../widgets/computer_tissue_section.dart | 394 ++++++++++++++++++ .../data/parsers/fit_import_parser.dart | 4 + .../data/parsers/ratio_xml_parser.dart | 51 +++ .../services/parsed_dive_profile_mapper.dart | 9 + .../data/services/shearwater_db_reader.dart | 194 ++++++++- .../data/services/shearwater_dive_mapper.dart | 83 +++- .../services/shearwater_gf99_aligner.dart | 92 ++++ lib/l10n/arb/app_ar.arb | 19 +- lib/l10n/arb/app_de.arb | 19 +- lib/l10n/arb/app_en.arb | 52 +++ lib/l10n/arb/app_es.arb | 19 +- lib/l10n/arb/app_fr.arb | 19 +- lib/l10n/arb/app_he.arb | 19 +- lib/l10n/arb/app_hu.arb | 19 +- lib/l10n/arb/app_it.arb | 19 +- lib/l10n/arb/app_localizations.dart | 105 +++++ lib/l10n/arb/app_localizations_ar.dart | 63 +++ lib/l10n/arb/app_localizations_de.dart | 64 +++ lib/l10n/arb/app_localizations_en.dart | 63 +++ lib/l10n/arb/app_localizations_es.dart | 64 +++ lib/l10n/arb/app_localizations_fr.dart | 64 +++ lib/l10n/arb/app_localizations_he.dart | 62 +++ lib/l10n/arb/app_localizations_hu.dart | 63 +++ lib/l10n/arb/app_localizations_it.dart | 64 +++ lib/l10n/arb/app_localizations_nl.dart | 64 +++ lib/l10n/arb/app_localizations_pt.dart | 63 +++ lib/l10n/arb/app_localizations_zh.dart | 62 +++ lib/l10n/arb/app_nl.arb | 19 +- lib/l10n/arb/app_pt.arb | 19 +- lib/l10n/arb/app_zh.arb | 19 +- test/core/constants/profile_metrics_test.dart | 4 + .../migration_v225_computer_tissue_test.dart | 90 ++++ .../database/profile_series_pack_test.dart | 2 +- .../uddf_gradient_factor_export_test.dart | 79 ++++ .../uddf_gradient_factor_import_test.dart | 110 +++++ .../uddf/uddf_gradient_factor_parse_test.dart | 44 ++ .../suunto_cloud/suunto_dive_parser_test.dart | 75 ++++ .../suunto_sml_normalizer_test.dart | 69 ++- .../suunto_tissue_parser_test.dart | 209 ++++++++++ .../sync/series_forward_version_test.dart | 12 +- .../services/dive_import_service_test.dart | 89 ++++ .../domain/entities/downloaded_dive_test.dart | 33 ++ .../fit/fit_profile_extractor_test.dart | 51 ++- .../fit/fit_summary_extractor_test.dart | 52 +++ .../fit/fit_tissue_snapshot_test.dart | 47 +++ .../services/fit_parser_service_test.dart | 33 ++ .../domain/entities/imported_dive_test.dart | 76 ++++ .../imported_dive_converter_test.dart | 67 +++ .../dive_computer_repository_tissue_test.dart | 124 ++++++ .../profile_series_repository_test.dart | 3 +- .../domain/codecs/profile_sample_test.dart | 6 +- .../codecs/profile_series_codec_test.dart | 54 ++- .../profile_series_field_table_test.dart | 28 +- .../codecs/profile_series_golden_test.dart | 37 +- .../computer_tissue_snapshot_test.dart | 216 ++++++++++ ...dive_profile_point_tissue_fields_test.dart | 49 +++ .../providers/computer_gf99_overlay_test.dart | 134 ++++++ .../widgets/computer_tissue_section_test.dart | 164 ++++++++ .../dive_sensor_summary_repository_test.dart | 6 +- .../services/sensor_summary_worker_test.dart | 6 +- .../adapters/suunto_cloud_adapter_test.dart | 36 ++ .../data/parsers/fit_import_parser_test.dart | 9 + .../fit_import_parser_tissue_test.dart | 45 ++ .../parsers/ratio_xml_parser_tissue_test.dart | 203 +++++++++ .../parsers/uddf_import_parser_gf99_test.dart | 27 ++ .../services/shearwater_db_reader_test.dart | 207 +++++++++ .../shearwater_dive_mapper_tissue_test.dart | 186 +++++++++ .../shearwater_gf99_aligner_test.dart | 123 ++++++ .../services/shearwater_test_helpers.dart | 132 ++++++ 108 files changed, 5645 insertions(+), 58 deletions(-) create mode 100644 lib/core/services/export/uddf/uddf_gradient_factor.dart create mode 100644 lib/core/services/suunto_cloud/suunto_tissue_parser.dart create mode 100644 lib/features/dive_import/data/services/fit/fit_tissue_snapshot.dart create mode 100644 lib/features/dive_log/domain/entities/computer_tissue_snapshot.dart create mode 100644 lib/features/dive_log/presentation/providers/computer_gf99_overlay.dart create mode 100644 lib/features/dive_log/presentation/widgets/computer_tissue_formatter.dart create mode 100644 lib/features/dive_log/presentation/widgets/computer_tissue_section.dart create mode 100644 lib/features/universal_import/data/services/shearwater_gf99_aligner.dart create mode 100644 test/core/database/migration_v225_computer_tissue_test.dart create mode 100644 test/core/services/export/uddf/uddf_gradient_factor_export_test.dart create mode 100644 test/core/services/export/uddf/uddf_gradient_factor_import_test.dart create mode 100644 test/core/services/export/uddf/uddf_gradient_factor_parse_test.dart create mode 100644 test/core/services/suunto_cloud/suunto_tissue_parser_test.dart create mode 100644 test/features/dive_computer/domain/entities/downloaded_dive_test.dart create mode 100644 test/features/dive_import/data/services/fit/fit_tissue_snapshot_test.dart create mode 100644 test/features/dive_log/data/repositories/dive_computer_repository_tissue_test.dart create mode 100644 test/features/dive_log/domain/entities/computer_tissue_snapshot_test.dart create mode 100644 test/features/dive_log/domain/entities/dive_profile_point_tissue_fields_test.dart create mode 100644 test/features/dive_log/presentation/providers/computer_gf99_overlay_test.dart create mode 100644 test/features/dive_log/presentation/widgets/computer_tissue_section_test.dart create mode 100644 test/features/universal_import/data/parsers/fit_import_parser_tissue_test.dart create mode 100644 test/features/universal_import/data/parsers/ratio_xml_parser_tissue_test.dart create mode 100644 test/features/universal_import/data/parsers/uddf_import_parser_gf99_test.dart create mode 100644 test/features/universal_import/data/services/shearwater_dive_mapper_tissue_test.dart create mode 100644 test/features/universal_import/data/services/shearwater_gf99_aligner_test.dart diff --git a/docs/superpowers/specs/2026-04-05-imported-profile-gap-priority-tracker.md b/docs/superpowers/specs/2026-04-05-imported-profile-gap-priority-tracker.md index 01853f9816..93db0694fd 100644 --- a/docs/superpowers/specs/2026-04-05-imported-profile-gap-priority-tracker.md +++ b/docs/superpowers/specs/2026-04-05-imported-profile-gap-priority-tracker.md @@ -56,6 +56,7 @@ Use the `Fixed` column as a working checkbox: | Rebreather dive fields (`setpointLow/High/Deco`, `SCR` config, diluent gas, loop O2, scrubber, loop volume) | [ ] | High | Yes | Yes | No | | Tank role / material metadata | [ ] | High | Yes | Yes | No[^1] | | Dive-level `cns` / `otu` | [x] | Medium | Yes | Yes | Yes | +| Sample `n2Load` / dive-level computer tissue snapshot (`computerTissue`: algorithm, start/end N2 and CNS) | [x] | Medium | Yes | N/A | N/A | | Dive-level deco metadata (`decoAlgorithm`, `GF low/high`, conservatism) | [ ] | Medium | Yes | No | Partial | | Profile events / markers | [ ] | Medium | Yes | Yes | Partial | | Source provenance snapshot (`DiveDataSources`) | [ ] | Medium | Yes | No | Yes | diff --git a/lib/core/constants/profile_metrics.dart b/lib/core/constants/profile_metrics.dart index 4b85dd66fd..52ffcd3bf2 100644 --- a/lib/core/constants/profile_metrics.dart +++ b/lib/core/constants/profile_metrics.dart @@ -212,4 +212,5 @@ typedef MetricSourceInfo = ({ MetricDataSource cnsActual, MetricDataSource decoStopActual, MetricDataSource gtrActual, + MetricDataSource gf99Actual, }); diff --git a/lib/core/database/database.dart b/lib/core/database/database.dart index 62f7de5b7f..1d0649f87f 100644 --- a/lib/core/database/database.dart +++ b/lib/core/database/database.dart @@ -812,6 +812,9 @@ class Dives extends Table { text().nullable()(); // "buhlmann", "vpm", "rgbm", "dciem" IntColumn get decoConservatism => integer().nullable()(); // Personal adjustment (0=neutral) + // Tissue state the dive computer itself reported (v225): the JSON of + // ComputerTissueSnapshot.toJson, or null when the source carried none. + TextColumn get computerTissueJson => text().nullable()(); // Dive computer that logged this dive (for display/export, separate from computerId relation) TextColumn get diveComputerModel => text().nullable()(); TextColumn get diveComputerSerial => text().nullable()(); @@ -4946,10 +4949,16 @@ class AppDatabase extends _$AppDatabase { // Renumbered from 223, which buddy profile links took while this was // in review. 224, + // v225: dives.computer_tissue_json, the tissue state a dive computer + // reports for the dive (import of Garmin, Shearwater, Suunto, Ratio and + // UDDF tissue data). Additive nullable column, no backfill. Takes 225, + // not 224: main shipped 224 (media fact clocks) while this branch was + // open, and a rung at or below the shipped version never runs its + // onUpgrade step, so this one sits above it. + 225, // v226: media.cloud_asset_id, the PhotoKit cloud identifier (media sync // program spec 6.2). Column only; the one-time backfill runs after a // sync, not here. Additive and nullable, so the floor stays at 224. - // 225 is held by PR #1978 (tissue loading import). 226, ]; @@ -6488,6 +6497,20 @@ class AppDatabase extends _$AppDatabase { } } + /// v225: dives.computer_tissue_json. Idempotent, so it is safe to call + /// from both onUpgrade and the beforeOpen backstop, and a no-op when the + /// table does not exist yet. + Future _assertComputerTissueColumn() async { + final cols = await customSelect("PRAGMA table_info('dives')").get(); + if (cols.isEmpty) return; + final names = cols.map((c) => c.read('name')).toSet(); + if (!names.contains('computer_tissue_json')) { + await customStatement( + 'ALTER TABLE dives ADD COLUMN computer_tissue_json TEXT', + ); + } + } + /// v126: emergency_chambers table + emergency card settings columns. /// Idempotent so it is safe to call from both onUpgrade and the /// beforeOpen backstop. @@ -12542,6 +12565,14 @@ class AppDatabase extends _$AppDatabase { await _backfillMediaFactClocks(); } if (from < 224) await reportProgress(); + // v225: dives.computer_tissue_json. Column-only rung, no backfill: + // null reads as "the computer reported no tissue state". Takes 225, + // not 224: main shipped 224 (media fact clocks) while this branch + // was open. + if (from < 225) { + await _assertComputerTissueColumn(); + } + if (from < 225) await reportProgress(); // v226: media.cloud_asset_id. Column only, no backfill. if (from < 226) { await _assertMediaCloudAssetIdColumn(); @@ -12929,6 +12960,11 @@ class AppDatabase extends _$AppDatabase { // arrives by restore or sync-adopt without them would throw on the // first read. await _assertBuddyProfileDiveLinkColumns(); + + // v225 backstop: re-assert dives.computer_tissue_json. Every dive + // read selects the whole row, so a database that arrives by restore + // or sync-adopt without it would throw on the first read. + await _assertComputerTissueColumn(); // v182 backstop: re-assert the packed profile series tables, then // pack any dive that still has legacy rows and no series row. A // schema-version collision with a parallel branch skips the rung on diff --git a/lib/core/database/legacy_sample_staging.dart b/lib/core/database/legacy_sample_staging.dart index 704a66c248..14816e301e 100644 --- a/lib/core/database/legacy_sample_staging.dart +++ b/lib/core/database/legacy_sample_staging.dart @@ -18,7 +18,13 @@ String _sqlType(ProfileFieldKind kind) => switch (kind) { ProfileFieldKind.runLengthString => 'TEXT', }; -/// The legacy `dive_profiles` columns: identity plus every codec field. +/// The legacy `dive_profiles` columns: identity plus every codec v1 field. +/// +/// Deliberately v1, not the newest table. These staging tables mirror the +/// row-per-sample `dive_profiles` table an older peer still publishes, and +/// that table only ever had the v1 columns: no peer below the v183 floor can +/// send `gf99` or `n2_load`, and the packer encodes staged rows with the +/// current codec anyway (the missing v2 columns simply pack as null). final List _legacyProfileColumns = [ 'id', 'dive_id', diff --git a/lib/core/services/export/uddf/uddf_export_builders.dart b/lib/core/services/export/uddf/uddf_export_builders.dart index 4d317db3fa..5ed69a33bc 100644 --- a/lib/core/services/export/uddf/uddf_export_builders.dart +++ b/lib/core/services/export/uddf/uddf_export_builders.dart @@ -365,6 +365,13 @@ class UddfExportBuilders { if (point.ppO2 != null) { builder.element('ppo2', nest: point.ppO2.toString()); } + // Computer-reported GF99 (UDDF 3.2 waypoint child). + if (point.gf99 != null) { + builder.element( + 'gradientfactor', + nest: point.gf99.toString(), + ); + } }, ); } diff --git a/lib/core/services/export/uddf/uddf_export_service.dart b/lib/core/services/export/uddf/uddf_export_service.dart index 74e651e032..3078cf1117 100644 --- a/lib/core/services/export/uddf/uddf_export_service.dart +++ b/lib/core/services/export/uddf/uddf_export_service.dart @@ -465,6 +465,15 @@ class UddfExportService { } } } + // Computer-reported GF99, as Shearwater + // Cloud writes it, so a round trip keeps + // the recorded value. + if (point.gf99 != null) { + builder.element( + 'gradientfactor', + nest: point.gf99.toString(), + ); + } }, ); } diff --git a/lib/core/services/export/uddf/uddf_full_import_service.dart b/lib/core/services/export/uddf/uddf_full_import_service.dart index e6108d3619..18b00ffe24 100644 --- a/lib/core/services/export/uddf/uddf_full_import_service.dart +++ b/lib/core/services/export/uddf/uddf_full_import_service.dart @@ -6,6 +6,7 @@ import 'package:submersion/core/services/logger_service.dart'; import 'package:submersion/core/services/export/models/uddf_import_result.dart'; import 'package:submersion/core/services/export/uddf/uddf_buddy_roles.dart'; import 'package:submersion/core/services/export/uddf/uddf_dump_codec.dart'; +import 'package:submersion/core/services/export/uddf/uddf_gradient_factor.dart'; import 'package:submersion/core/services/export/uddf/uddf_import_parsers.dart'; import 'package:submersion/features/universal_import/data/services/import_site_location.dart'; import 'package:submersion/core/services/export/uddf/uddf_normalizer.dart'; @@ -2512,6 +2513,16 @@ class UddfFullImportService { point['ndl'] = UddfImportParsers.parseUddfInt(ndlText); } + // Shearwater Cloud and Subsurface write the computer's GF99 on each + // waypoint. Only set when present, so a sample without one carries + // no key rather than a null. + final gf99 = parseUddfGradientFactorPercent( + UddfImportParsers.getElementText(waypoint, 'gradientfactor'), + ); + if (gf99 != null) { + point['gf99'] = gf99; + } + final decoStop = waypoint.findElements('decostop').firstOrNull; if (decoStop != null) { final kind = decoStop.getAttribute('kind')?.trim().toLowerCase(); diff --git a/lib/core/services/export/uddf/uddf_gradient_factor.dart b/lib/core/services/export/uddf/uddf_gradient_factor.dart new file mode 100644 index 0000000000..2e4e17b0fe --- /dev/null +++ b/lib/core/services/export/uddf/uddf_gradient_factor.dart @@ -0,0 +1,21 @@ +/// Reads a UDDF waypoint `` as GF99, a whole percent. +/// +/// Shearwater Cloud and Subsurface write whole percents ("0", "63"). Some +/// writers use a fraction of one instead ("0.63"); a value with a decimal +/// point in (0, 1] is read as such and scaled to percent. Nothing is +/// clamped, so a supersaturated 120 stays 120. Blank, non-numeric and +/// non-finite text read as null. +int? parseUddfGradientFactorPercent(String? text) { + if (text == null) return null; + final trimmed = text.trim(); + if (trimmed.isEmpty) return null; + + final asInt = int.tryParse(trimmed); + if (asInt != null) return asInt; + + final asDouble = double.tryParse(trimmed); + if (asDouble == null || !asDouble.isFinite) return null; + + final isFraction = trimmed.contains('.') && asDouble > 0 && asDouble <= 1.0; + return isFraction ? (asDouble * 100).round() : asDouble.round(); +} diff --git a/lib/core/services/export/uddf/uddf_import_service.dart b/lib/core/services/export/uddf/uddf_import_service.dart index 6cb094a001..7b762082ea 100644 --- a/lib/core/services/export/uddf/uddf_import_service.dart +++ b/lib/core/services/export/uddf/uddf_import_service.dart @@ -1,6 +1,7 @@ import 'package:xml/xml.dart'; import 'package:submersion/core/services/logger_service.dart'; +import 'package:submersion/core/services/export/uddf/uddf_gradient_factor.dart'; import 'package:submersion/core/services/export/uddf/uddf_import_parsers.dart'; import 'package:submersion/features/universal_import/data/services/import_site_location.dart'; import 'package:submersion/features/dive_log/domain/entities/dive.dart'; @@ -695,6 +696,14 @@ class UddfImportService { point['heartRate'] = UddfImportParsers.parseUddfInt(heartRateText); } + // Computer-reported GF99 (whole percent), set only when present. + final gf99 = parseUddfGradientFactorPercent( + _getElementText(waypoint, 'gradientfactor'), + ); + if (gf99 != null) { + point['gf99'] = gf99; + } + if (point.containsKey('timestamp') && point.containsKey('depth')) { profile.add(point); } diff --git a/lib/core/services/suunto_cloud/suunto_dive_parser.dart b/lib/core/services/suunto_cloud/suunto_dive_parser.dart index d474459ad5..2a37e222cf 100644 --- a/lib/core/services/suunto_cloud/suunto_dive_parser.dart +++ b/lib/core/services/suunto_cloud/suunto_dive_parser.dart @@ -1,7 +1,9 @@ import 'dart:math' as math; import 'package:submersion/core/services/suunto_cloud/suunto_cloud_event_map.dart'; +import 'package:submersion/core/services/suunto_cloud/suunto_tissue_parser.dart'; import 'package:submersion/features/dive_computer/domain/entities/downloaded_dive.dart'; +import 'package:submersion/features/dive_log/domain/entities/computer_tissue_snapshot.dart'; /// A dive parsed from a Suunto export, plus the device identity fields /// needed to resolve/create the owning [DiveComputer] record (kept separate @@ -16,6 +18,11 @@ class SuuntoParsedDive { final DownloadedDive dive; + /// Dive-level tissue state the computer reported in the SML header + /// (`Header.Diving.StartTissue` / `EndTissue` / `Algorithm`), if any. + /// Lives on [dive] so the shared import pipeline persists it. + ComputerTissueSnapshot? get computerTissue => dive.computerTissue; + /// Suunto's internal device codename (e.g. "Vaasa"), already mapped to a /// commercial product line name (e.g. "Suunto Nautic") for display. final String? deviceName; @@ -96,6 +103,7 @@ class SuuntoDiveParser { final diving = header['Diving'] as Map?; final gfLow = (diving?['GfLow'] as num?)?.round(); final gfHigh = (diving?['GfHigh'] as num?)?.round(); + final computerTissue = diving == null ? null : parseSuuntoTissue(diving); final tanks = _buildTanks(diving, profileResult.gasSwitchOrder); @@ -125,6 +133,7 @@ class SuuntoDiveParser { gfLow: gfLow, gfHigh: gfHigh, decoAlgorithm: (gfLow != null && gfHigh != null) ? 'buhlmann' : null, + computerTissue: computerTissue, events: profileResult.events, ); diff --git a/lib/core/services/suunto_cloud/suunto_sml_normalizer.dart b/lib/core/services/suunto_cloud/suunto_sml_normalizer.dart index 1ec8b4fdaf..5e7b3415ff 100644 --- a/lib/core/services/suunto_cloud/suunto_sml_normalizer.dart +++ b/lib/core/services/suunto_cloud/suunto_sml_normalizer.dart @@ -24,6 +24,13 @@ class SuuntoSmlNormalizer { static const int _suuntoActivityScuba = 51; + /// DiveHeader fields hoisted verbatim onto `header['Diving']`. + static const List _diveHeaderTissueKeys = [ + 'StartTissue', + 'EndTissue', + 'Algorithm', + ]; + /// Throws [SuuntoApiException] if [json] isn't a recognizable dive export /// (neither cloud 'sml' nor app 'DeviceLog' shape, or not a dive activity). static SuuntoDiveExport parse(Map json) { @@ -117,6 +124,14 @@ class SuuntoSmlNormalizer { diving['GfLow'] = diveHeader['LowGf']; diving['GfHigh'] = diveHeader['HighGf']; } + // The dive-level tissue state (per-compartment tensions, CNS/OTU, RGBM + // factors) and the deco model name sit in the app's DeviceLog schema + // under Header.Diving already; the cloud shape keeps them on the + // DiveHeader, so carry them across under the same names for the + // tissue parser. + for (final key in _diveHeaderTissueKeys) { + if (diveHeader[key] != null) diving[key] = diveHeader[key]; + } if (diving.isNotEmpty) header['Diving'] = diving; // The surface GPS pair (Start = entry, Stop = exit, both in radians) diff --git a/lib/core/services/suunto_cloud/suunto_tissue_parser.dart b/lib/core/services/suunto_cloud/suunto_tissue_parser.dart new file mode 100644 index 0000000000..01a0afa2b7 --- /dev/null +++ b/lib/core/services/suunto_cloud/suunto_tissue_parser.dart @@ -0,0 +1,80 @@ +import 'package:submersion/core/utils/number_utils.dart'; +import 'package:submersion/features/dive_log/domain/entities/computer_tissue_snapshot.dart'; + +/// Reads the dive-level tissue state a Suunto SML header carries under +/// `Header.Diving` (`StartTissue`, `EndTissue`, `Algorithm`) into a +/// [ComputerTissueSnapshot]. +/// +/// Suunto reports per-compartment tensions in Pascal (9 compartments on the +/// HelO2 / D-series RGBM, 15 on the EON Fused2 RGBM); they come out in bar +/// with five decimals. `CNS` (and its `OLF` stand-in on older models) is a +/// 0-1 fraction and comes out in percent; `OTU` and the two RGBM factors are +/// kept as reported. +/// +/// Two encodings of the compartment arrays exist and both are accepted: a +/// plain numeric list (the app's DeviceLog JSON export), or -- in JSON +/// derived from the SML XML -- a `{"Pressure": [...]}` object or a list of +/// `{"Pressure": n}` maps. +/// +/// A state with no numeric content is skipped; the result is null when +/// neither state has anything, even if `Algorithm` is present. Never throws. +ComputerTissueSnapshot? parseSuuntoTissue(Map diving) { + final start = _stateOf(diving['StartTissue']); + final end = _stateOf(diving['EndTissue']); + if (start == null && end == null) return null; + + final algorithm = diving['Algorithm']; + return ComputerTissueSnapshot( + algorithm: algorithm is String && algorithm.isNotEmpty ? algorithm : null, + start: start, + end: end, + ); +} + +const double _pascalPerBar = 100000; + +ComputerTissueState? _stateOf(Object? value) { + if (value is! Map) return null; + final tissue = Map.from(value); + + final cns = asDoubleOrNull(tissue['CNS']) ?? asDoubleOrNull(tissue['OLF']); + final state = ComputerTissueState( + n2Bar: _tensionsBar(tissue['Nitrogen']), + heBar: _tensionsBar(tissue['Helium']), + cnsPercent: cns == null ? null : cns * 100, + otu: asDoubleOrNull(tissue['OTU']), + rgbmNitrogen: asDoubleOrNull(tissue['RgbmNitrogen']), + rgbmHelium: asDoubleOrNull(tissue['RgbmHelium']), + ); + + final hasContent = + state.n2Bar != null || + state.heBar != null || + state.cnsPercent != null || + state.otu != null || + state.rgbmNitrogen != null || + state.rgbmHelium != null; + return hasContent ? state : null; +} + +/// Per-compartment tensions in bar, or null when [value] holds none. +/// +/// Compartment order is positional, so a list with any non-numeric element +/// reads as null as a whole rather than as a shorter list. +List? _tensionsBar(Object? value) { + if (value is Map) return _tensionsBar(value['Pressure']); + if (value is! List || value.isEmpty) return null; + + final bars = []; + for (final element in value) { + final pascal = asDoubleOrNull( + element is Map ? element['Pressure'] : element, + ); + if (pascal == null) return null; + bars.add(_pascalToBar(pascal)); + } + return List.unmodifiable(bars); +} + +/// Pascal to bar, kept to five decimals (whole-Pascal precision). +double _pascalToBar(double pascal) => pascal.round() / _pascalPerBar; diff --git a/lib/features/dive_3d/application/tissue_providers.dart b/lib/features/dive_3d/application/tissue_providers.dart index ce0375321f..40bd91c1d5 100644 --- a/lib/features/dive_3d/application/tissue_providers.dart +++ b/lib/features/dive_3d/application/tissue_providers.dart @@ -12,6 +12,11 @@ import 'package:submersion/features/settings/presentation/providers/settings_pro /// The per-sample decompression status series for a dive's active source - /// the same data that feeds the 2D tissue heat map. +/// +/// Always the app's own Buhlmann recompute. An imported +/// `Dive.computerTissue` snapshot is dive-level (start and end only) and its +/// compartment model differs by computer, so it cannot seed this per-sample +/// scene and is deliberately not used here. final tissueDecoStatusesProvider = FutureProvider.family, String>((ref, diveId) async { final analysis = await ref.watch( diff --git a/lib/features/dive_computer/data/services/dive_import_service.dart b/lib/features/dive_computer/data/services/dive_import_service.dart index 147ab6675f..4a3acd6f46 100644 --- a/lib/features/dive_computer/data/services/dive_import_service.dart +++ b/lib/features/dive_computer/data/services/dive_import_service.dart @@ -760,6 +760,7 @@ class DiveImportService { gfLow: dive.gfLow, gfHigh: dive.gfHigh, decoConservatism: dive.decoConservatism, + computerTissue: dive.computerTissue, diveMode: dive.diveMode, diluentO2: dive.diluentO2, diluentHe: dive.diluentHe, @@ -860,6 +861,7 @@ class DiveImportService { gfLow: dive.gfLow, gfHigh: dive.gfHigh, decoConservatism: dive.decoConservatism, + computerTissue: dive.computerTissue, diveMode: dive.diveMode, diluentO2: dive.diluentO2, diluentHe: dive.diluentHe, diff --git a/lib/features/dive_computer/domain/entities/downloaded_dive.dart b/lib/features/dive_computer/domain/entities/downloaded_dive.dart index 362fbb31ec..d0b4e913a2 100644 --- a/lib/features/dive_computer/domain/entities/downloaded_dive.dart +++ b/lib/features/dive_computer/domain/entities/downloaded_dive.dart @@ -1,6 +1,7 @@ import 'dart:typed_data'; import 'package:submersion/core/constants/enums.dart'; +import 'package:submersion/features/dive_log/domain/entities/computer_tissue_snapshot.dart'; /// Phases of the download process. enum DownloadPhase { @@ -139,6 +140,11 @@ class DownloadedDive { /// Personal deco conservatism adjustment final int? decoConservatism; + /// Dive-level tissue state the computer itself reported (start/end + /// compartment loadings, CNS, OTU), when the source carries one. Never + /// computed by the app; persisted verbatim on the dive. + final ComputerTissueSnapshot? computerTissue; + /// Dive events from the computer final List events; @@ -180,6 +186,7 @@ class DownloadedDive { this.gfLow, this.gfHigh, this.decoConservatism, + this.computerTissue, this.diveMode = DiveMode.oc, this.events = const [], this.rawData, diff --git a/lib/features/dive_import/data/services/dive_reimport_service.dart b/lib/features/dive_import/data/services/dive_reimport_service.dart index 8be716ee3c..6335a80022 100644 --- a/lib/features/dive_import/data/services/dive_reimport_service.dart +++ b/lib/features/dive_import/data/services/dive_reimport_service.dart @@ -590,6 +590,8 @@ class DiveReimportService { ndl: p['ndl'] as int?, tts: p['tts'] as int?, ceiling: _asDouble(p['ceiling']), + gf99: p['gf99'] as int?, + n2Load: p['n2Load'] as int?, ), ], now: now.millisecondsSinceEpoch, diff --git a/lib/features/dive_import/data/services/fit/fit_profile_extractor.dart b/lib/features/dive_import/data/services/fit/fit_profile_extractor.dart index 7e76580319..030061b34f 100644 --- a/lib/features/dive_import/data/services/fit/fit_profile_extractor.dart +++ b/lib/features/dive_import/data/services/fit/fit_profile_extractor.dart @@ -11,6 +11,7 @@ class FitSample { this.ndlSeconds, this.ttsSeconds, this.cns, + this.n2Load, }); final int timestampMs; // Unix milliseconds. @@ -21,14 +22,21 @@ class FitSample { final int? ndlSeconds; // record.ndlTime final int? ttsSeconds; // record.timeToSurface final double? cns; // percent (record.cnsLoad) + final int? n2Load; // percent, aggregate N2 tissue loading (record.n2Load) } /// Extracts per-sample dive profile data from `record` messages, including the -/// Garmin-recorded deco values (ceiling/TTS/NDL/CNS) which are imported as -/// recorded, never recomputed. Records without depth or timestamp are skipped. +/// Garmin-recorded deco values (ceiling/TTS/NDL/CNS/N2 loading) which are +/// imported as recorded, never recomputed. Records without depth or timestamp +/// are skipped. class FitProfileExtractor { const FitProfileExtractor._(); + /// Upper bound for a plausible `n2_load` percent. fit_tool already reads + /// the uint16 invalid sentinel (0xFFFF) as null; anything above this is a + /// corrupt or unknown encoding and is dropped rather than stored. + static const _maxN2LoadPercent = 1000; + static List extract(List records) { final samples = []; for (final r in records) { @@ -45,9 +53,13 @@ class FitProfileExtractor { ndlSeconds: r.ndlTime, ttsSeconds: r.timeToSurface, cns: r.cnsLoad?.toDouble(), + n2Load: _validN2Load(r.n2Load), ), ); } return samples; } + + static int? _validN2Load(int? value) => + value == null || value < 0 || value > _maxN2LoadPercent ? null : value; } diff --git a/lib/features/dive_import/data/services/fit/fit_summary_extractor.dart b/lib/features/dive_import/data/services/fit/fit_summary_extractor.dart index da9eefebd7..44ee872d68 100644 --- a/lib/features/dive_import/data/services/fit/fit_summary_extractor.dart +++ b/lib/features/dive_import/data/services/fit/fit_summary_extractor.dart @@ -8,6 +8,8 @@ class FitSummary { this.surfaceInterval, this.cnsStart, this.cnsEnd, + this.startN2, + this.endN2, this.otu, this.entryLat, this.entryLong, @@ -22,6 +24,8 @@ class FitSummary { final Duration? surfaceInterval; final double? cnsStart; final double? cnsEnd; + final int? startN2; // percent, aggregate N2 loading at dive start + final int? endN2; // percent, aggregate N2 loading at dive end final double? otu; final double? entryLat; // degrees final double? entryLong; // degrees @@ -50,6 +54,8 @@ class FitSummaryExtractor { surfaceInterval: secs(summary?.surfaceInterval), cnsStart: summary?.startCns?.toDouble(), cnsEnd: summary?.endCns?.toDouble(), + startN2: summary?.startN2, + endN2: summary?.endN2, otu: summary?.o2Toxicity?.toDouble(), entryLat: session?.startPositionLat, entryLong: session?.startPositionLong, diff --git a/lib/features/dive_import/data/services/fit/fit_tissue_snapshot.dart b/lib/features/dive_import/data/services/fit/fit_tissue_snapshot.dart new file mode 100644 index 0000000000..f0a6702bb2 --- /dev/null +++ b/lib/features/dive_import/data/services/fit/fit_tissue_snapshot.dart @@ -0,0 +1,31 @@ +import 'package:submersion/features/dive_import/data/services/fit/fit_summary_extractor.dart'; +import 'package:submersion/features/dive_log/domain/entities/computer_tissue_snapshot.dart'; + +/// The computer-reported tissue state a Garmin `dive_summary` carries: +/// aggregate N2 loading (percent) at dive start and end, with the CNS +/// readings alongside and the configured deco model as the algorithm. +/// +/// Null when the summary has no N2 loading on either side, so dives from +/// devices that do not record it keep `computerTissue` unset. A side whose +/// N2 value is missing is left null rather than filled from CNS alone. +ComputerTissueSnapshot? fitTissueSnapshot(FitSummary summary) { + final startN2 = summary.startN2; + final endN2 = summary.endN2; + if (startN2 == null && endN2 == null) return null; + + return ComputerTissueSnapshot( + algorithm: summary.decoModel, + start: startN2 == null + ? null + : ComputerTissueState( + n2LoadPercent: startN2.toDouble(), + cnsPercent: summary.cnsStart, + ), + end: endN2 == null + ? null + : ComputerTissueState( + n2LoadPercent: endN2.toDouble(), + cnsPercent: summary.cnsEnd, + ), + ); +} diff --git a/lib/features/dive_import/data/services/fit_parser_service.dart b/lib/features/dive_import/data/services/fit_parser_service.dart index c6d7f3c5c6..f75f2c84d2 100644 --- a/lib/features/dive_import/data/services/fit_parser_service.dart +++ b/lib/features/dive_import/data/services/fit_parser_service.dart @@ -12,6 +12,7 @@ import 'package:submersion/features/dive_import/data/services/fit/fit_profile_ex import 'package:submersion/features/dive_import/data/services/fit/fit_summary_extractor.dart'; import 'package:submersion/features/dive_import/data/services/fit/fit_tank_extractor.dart'; import 'package:submersion/features/dive_import/data/services/fit/fit_time_resolver.dart'; +import 'package:submersion/features/dive_import/data/services/fit/fit_tissue_snapshot.dart'; import 'package:submersion/features/dive_import/domain/entities/imported_dive.dart'; /// Garmin FIT sport codes that represent dive activities. @@ -190,6 +191,7 @@ class FitParserService { surfaceIntervalSeconds: summary.surfaceInterval?.inSeconds, cnsStart: summary.cnsStart, cnsEnd: summary.cnsEnd, + computerTissue: fitTissueSnapshot(summary), otu: summary.otu, waterType: summary.waterType, decoModel: summary.decoModel, @@ -276,6 +278,7 @@ class FitParserService { temperature: s.temperature, heartRate: s.heartRate, cns: s.cns, + n2Load: s.n2Load, ndlSeconds: s.ndlSeconds, ttsSeconds: s.ttsSeconds, ceiling: s.ceiling, diff --git a/lib/features/dive_import/data/services/uddf_entity_importer.dart b/lib/features/dive_import/data/services/uddf_entity_importer.dart index eca8e51e87..d991cbf050 100644 --- a/lib/features/dive_import/data/services/uddf_entity_importer.dart +++ b/lib/features/dive_import/data/services/uddf_entity_importer.dart @@ -34,6 +34,7 @@ import 'package:submersion/features/dive_centers/domain/entities/dive_center.dar import 'package:submersion/features/dive_log/data/repositories/dive_computer_repository_impl.dart'; import 'package:submersion/features/dive_log/data/repositories/dive_repository_impl.dart'; import 'package:submersion/features/dive_log/data/repositories/tank_pressure_repository.dart'; +import 'package:submersion/features/dive_log/domain/entities/computer_tissue_snapshot.dart'; import 'package:submersion/features/dive_log/domain/entities/dive.dart'; import 'package:submersion/features/dive_log/domain/entities/dive_custom_field.dart'; import 'package:submersion/features/dive_log/domain/entities/dive_weight.dart'; @@ -2414,6 +2415,11 @@ class UddfEntityImporter { o2Sensor6: asDoubleOrNull(p['o2Sensor6']), // UDDF carries no millivolt field; these arrive only via the // libdivecomputer path that shares this map (issue #810). + // Computer-reported tissue loading, whole percents. Parsers + // that have them (FIT n2_load, Shearwater/UDDF GF99) set + // these keys; the rest leave them absent. + gf99: p['gf99'] as int?, + n2Load: p['n2Load'] as int?, o2SensorMv1: p['o2SensorMv1'] as int?, o2SensorMv2: p['o2SensorMv2'] as int?, o2SensorMv3: p['o2SensorMv3'] as int?, @@ -2614,6 +2620,9 @@ class UddfEntityImporter { decoAlgorithm: diveData['decoAlgorithm'] as String?, gradientFactorLow: diveData['gradientFactorLow'] as int?, gradientFactorHigh: diveData['gradientFactorHigh'] as int?, + // Dive-level tissue state the computer reported, as the parser + // found it (a ComputerTissueSnapshot, or its JSON map or text). + computerTissue: ComputerTissueSnapshot.from(diveData['computerTissue']), diveComputerModel: diveData['diveComputerModel'] as String?, diveComputerSerial: diveData['diveComputerSerial'] as String?, diveComputerFirmware: diveData['diveComputerFirmware'] as String?, diff --git a/lib/features/dive_import/domain/entities/imported_dive.dart b/lib/features/dive_import/domain/entities/imported_dive.dart index ed49228b53..ef3705da02 100644 --- a/lib/features/dive_import/domain/entities/imported_dive.dart +++ b/lib/features/dive_import/domain/entities/imported_dive.dart @@ -1,5 +1,7 @@ import 'package:equatable/equatable.dart'; +import 'package:submersion/features/dive_log/domain/entities/computer_tissue_snapshot.dart'; + /// Source of imported dive data enum ImportSource { appleWatch, garmin, suunto, uddf } @@ -125,6 +127,10 @@ class ImportedDive extends Equatable { final String? sourceFileName; final String? sourceFileFormat; + /// Tissue state the dive computer itself reported, when the source + /// carried one. Never computed here. + final ComputerTissueSnapshot? computerTissue; + const ImportedDive({ required this.sourceId, this.sourceUuid, @@ -158,6 +164,7 @@ class ImportedDive extends Equatable { required this.profile, this.sourceFileName, this.sourceFileFormat, + this.computerTissue, }); Duration get duration => endTime.difference(startTime); @@ -197,6 +204,7 @@ class ImportedDive extends Equatable { profile, sourceFileName, sourceFileFormat, + computerTissue, ]; } @@ -212,6 +220,12 @@ class ImportedProfileSample extends Equatable { final double? ceiling; final List? tankPressures; + /// Computer-reported GF99 at this sample, whole percent. + final int? gf99; + + /// Computer-reported aggregate N2 tissue loading, whole percent. + final int? n2Load; + const ImportedProfileSample({ required this.timeSeconds, required this.depth, @@ -222,6 +236,8 @@ class ImportedProfileSample extends Equatable { this.ttsSeconds, this.ceiling, this.tankPressures, + this.gf99, + this.n2Load, }); ImportedProfileSample copyWith({ @@ -234,6 +250,8 @@ class ImportedProfileSample extends Equatable { int? ttsSeconds, double? ceiling, List? tankPressures, + int? gf99, + int? n2Load, }) { return ImportedProfileSample( timeSeconds: timeSeconds ?? this.timeSeconds, @@ -245,6 +263,8 @@ class ImportedProfileSample extends Equatable { ttsSeconds: ttsSeconds ?? this.ttsSeconds, ceiling: ceiling ?? this.ceiling, tankPressures: tankPressures ?? this.tankPressures, + gf99: gf99 ?? this.gf99, + n2Load: n2Load ?? this.n2Load, ); } @@ -259,5 +279,7 @@ class ImportedProfileSample extends Equatable { ttsSeconds, ceiling, tankPressures, + gf99, + n2Load, ]; } diff --git a/lib/features/dive_import/domain/services/imported_dive_converter.dart b/lib/features/dive_import/domain/services/imported_dive_converter.dart index 75b3d551ff..674ac4536a 100644 --- a/lib/features/dive_import/domain/services/imported_dive_converter.dart +++ b/lib/features/dive_import/domain/services/imported_dive_converter.dart @@ -54,6 +54,7 @@ class ImportedDiveConverter { profile: profile, importSource: sourceName, importId: importedDive.sourceId, + computerTissue: importedDive.computerTissue, ); // Calculate bottom time from profile if available @@ -77,6 +78,8 @@ class ImportedDiveConverter { temperature: sample.temperature, heartRate: sample.heartRate, heartRateSource: sample.heartRate != null ? sourceName : null, + gf99: sample.gf99, + n2Load: sample.n2Load, ); }).toList(); } diff --git a/lib/features/dive_log/data/repositories/dive_computer_repository_impl.dart b/lib/features/dive_log/data/repositories/dive_computer_repository_impl.dart index fb8e482f78..658b5e1657 100644 --- a/lib/features/dive_log/data/repositories/dive_computer_repository_impl.dart +++ b/lib/features/dive_log/data/repositories/dive_computer_repository_impl.dart @@ -25,6 +25,7 @@ import 'package:submersion/features/dive_log/data/repositories/profile_series_re import 'package:submersion/features/dive_log/data/repositories/safety_findings_repository.dart'; import 'package:submersion/features/dive_log/data/repositories/series_id_chunks.dart'; import 'package:submersion/features/dive_log/data/repositories/tank_pressure_series_repository.dart'; +import 'package:submersion/features/dive_log/domain/entities/computer_tissue_snapshot.dart'; import 'package:submersion/features/dive_sites/domain/entities/dive_site.dart' show GeoPoint; import 'package:submersion/features/dive_log/domain/codecs/profile_sample.dart' @@ -1301,6 +1302,9 @@ class DiveComputerRepository { // same way every other field in that branch is left untouched. double? diluentO2, double? diluentHe, + // Tissue state the computer reported for the dive as a whole; stored + // verbatim. Null on an update leaves any stored snapshot in place. + ComputerTissueSnapshot? computerTissue, List? events, List? gasSwitches, int? diveNumber, @@ -1412,6 +1416,7 @@ class DiveComputerRepository { gradientFactorHigh: Value(gfHigh), decoAlgorithm: Value(decoAlgorithm), decoConservatism: Value(decoConservatism), + computerTissueJson: Value(computerTissue?.encode()), diveMode: Value(diveMode.code), // Only set when the caller resolved a Diluent cylinder, // never a fabricated default -- an OC dive or a CCR dive @@ -1877,6 +1882,9 @@ class DiveComputerRepository { gradientFactorHigh: Value(gfHigh), decoAlgorithm: Value(decoAlgorithm), decoConservatism: Value(decoConservatism), + computerTissueJson: computerTissue == null + ? const Value.absent() + : Value(computerTissue.encode()), ), ); await _syncRepository.markRecordPending( @@ -2329,6 +2337,8 @@ class DiveComputerRepository { o2SensorMv4: p.o2SensorMv4, o2SensorMv5: p.o2SensorMv5, o2SensorMv6: p.o2SensorMv6, + gf99: p.gf99, + n2Load: p.n2Load, ); /// Calculate bottom time (seconds) from profile points. @@ -2531,6 +2541,13 @@ class ProfilePointData { final int? o2SensorMv5; final int? o2SensorMv6; + /// Computer-reported GF99, whole percent. libdivecomputer never reports + /// it; file importers may. + final int? gf99; + + /// Computer-reported aggregate N2 tissue loading, whole percent. + final int? n2Load; + const ProfilePointData({ required this.timestamp, required this.depth, @@ -2563,6 +2580,8 @@ class ProfilePointData { this.o2SensorMv4, this.o2SensorMv5, this.o2SensorMv6, + this.gf99, + this.n2Load, }); } diff --git a/lib/features/dive_log/data/repositories/dive_repository_impl.dart b/lib/features/dive_log/data/repositories/dive_repository_impl.dart index 47cd33357b..1771bd9b51 100644 --- a/lib/features/dive_log/data/repositories/dive_repository_impl.dart +++ b/lib/features/dive_log/data/repositories/dive_repository_impl.dart @@ -19,6 +19,7 @@ import 'package:submersion/features/dive_log/data/services/data_source_strand.da import 'package:submersion/features/dive_log/domain/codecs/profile_sample_point.dart'; import 'package:submersion/features/dive_log/domain/entities/bulk_edit_request.dart' as domain; +import 'package:submersion/features/dive_log/domain/entities/computer_tissue_snapshot.dart'; import 'package:submersion/features/dive_log/domain/entities/dive.dart' as domain; import 'package:submersion/features/dive_log/domain/entities/dive_data_source.dart'; @@ -1495,6 +1496,7 @@ class DiveRepository { gradientFactorHigh: Value(dive.gradientFactorHigh), decoAlgorithm: Value(dive.decoAlgorithm), decoConservatism: Value(dive.decoConservatism), + computerTissueJson: Value(dive.computerTissue?.encode()), diveComputerModel: Value(dive.diveComputerModel), diveComputerSerial: Value(dive.diveComputerSerial), diveComputerFirmware: Value(dive.diveComputerFirmware), @@ -1792,6 +1794,7 @@ class DiveRepository { gradientFactorHigh: Value(dive.gradientFactorHigh), decoAlgorithm: Value(dive.decoAlgorithm), decoConservatism: Value(dive.decoConservatism), + computerTissueJson: Value(dive.computerTissue?.encode()), diveComputerModel: Value(dive.diveComputerModel), diveComputerSerial: Value(dive.diveComputerSerial), diveComputerFirmware: Value(dive.diveComputerFirmware), @@ -3893,6 +3896,7 @@ class DiveRepository { gradientFactorHigh: row.gradientFactorHigh, decoAlgorithm: row.decoAlgorithm, decoConservatism: row.decoConservatism, + computerTissue: ComputerTissueSnapshot.tryDecode(row.computerTissueJson), diveComputerModel: row.diveComputerModel, diveComputerSerial: row.diveComputerSerial, diveComputerFirmware: row.diveComputerFirmware, @@ -4316,6 +4320,7 @@ class DiveRepository { gradientFactorHigh: row.gradientFactorHigh, decoAlgorithm: row.decoAlgorithm, decoConservatism: row.decoConservatism, + computerTissue: ComputerTissueSnapshot.tryDecode(row.computerTissueJson), diveComputerModel: row.diveComputerModel, diveComputerSerial: row.diveComputerSerial, diveComputerFirmware: row.diveComputerFirmware, diff --git a/lib/features/dive_log/data/services/profile_analysis_service.dart b/lib/features/dive_log/data/services/profile_analysis_service.dart index b81ff54c73..2e1f66cf24 100644 --- a/lib/features/dive_log/data/services/profile_analysis_service.dart +++ b/lib/features/dive_log/data/services/profile_analysis_service.dart @@ -299,6 +299,13 @@ class ProfileAnalysis { /// Cumulative CNS% at each profile point (includes residual from prior dives) final List? cnsCurve; + /// Aggregate N2 tissue loading (percent) as the dive computer reported it + /// per sample, null entries where it reported nothing. A computer-only + /// metric with no calculated counterpart: null unless the overlay in + /// profile_analysis_provider.dart found any (see + /// [DiveProfilePoint.n2Load]). + final List? n2LoadCurve; + /// Cumulative OTU at each profile point final List? otuCurve; @@ -357,6 +364,7 @@ class ProfileAnalysis { this.ttsCurve, this.gtrCurve, this.cnsCurve, + this.n2LoadCurve, this.otuCurve, required this.maxDepth, required this.averageDepth, @@ -455,6 +463,7 @@ class ProfileAnalysis { List? ttsCurve, List? gtrCurve, List? cnsCurve, + List? n2LoadCurve, List? otuCurve, double? maxDepth, double? averageDepth, @@ -490,6 +499,7 @@ class ProfileAnalysis { ttsCurve: ttsCurve ?? this.ttsCurve, gtrCurve: gtrCurve ?? this.gtrCurve, cnsCurve: cnsCurve ?? this.cnsCurve, + n2LoadCurve: n2LoadCurve ?? this.n2LoadCurve, otuCurve: otuCurve ?? this.otuCurve, maxDepth: maxDepth ?? this.maxDepth, averageDepth: averageDepth ?? this.averageDepth, diff --git a/lib/features/dive_log/domain/codecs/profile_field_table.dart b/lib/features/dive_log/domain/codecs/profile_field_table.dart index 4529d0a8be..bf52ff3d68 100644 --- a/lib/features/dive_log/domain/codecs/profile_field_table.dart +++ b/lib/features/dive_log/domain/codecs/profile_field_table.dart @@ -54,3 +54,14 @@ const List kProfileFieldTableV1 = [ ProfileField('o2_sensor_mv5', ProfileFieldKind.deltaInt), ProfileField('o2_sensor_mv6', ProfileFieldKind.deltaInt), ]; + +/// Codec v2: v1 plus the tissue-loading figures a dive computer reports per +/// sample, both whole percents. `gf99` is the computer's own GF99 (Shearwater, +/// UDDF ``); `n2_load` its aggregate N2 tissue loading +/// (Garmin FIT `n2_load`). Same freeze rule as v1: append under a new +/// version, never edit. +const List kProfileFieldTableV2 = [ + ...kProfileFieldTableV1, + ProfileField('gf99', ProfileFieldKind.deltaInt), + ProfileField('n2_load', ProfileFieldKind.deltaInt), +]; diff --git a/lib/features/dive_log/domain/codecs/profile_sample.dart b/lib/features/dive_log/domain/codecs/profile_sample.dart index ed65e6470a..0d32550727 100644 --- a/lib/features/dive_log/domain/codecs/profile_sample.dart +++ b/lib/features/dive_log/domain/codecs/profile_sample.dart @@ -2,7 +2,8 @@ import 'package:equatable/equatable.dart'; /// One profile sample exactly as the v181 `dive_profiles` table stored it /// (the codec's column order), minus the identity columns (`id`, `dive_id`, -/// `computer_id`, `source_id`, `is_primary`) that live on the series row. +/// `computer_id`, `source_id`, `is_primary`) that live on the series row, +/// plus the codec v2 fields ([gf99], [n2Load]) appended after them. /// /// This is the codec's input and output type. It differs from the domain /// `DiveProfilePoint` in one field: the legacy per-sample [pressure] @@ -44,6 +45,8 @@ class ProfileSample extends Equatable { this.o2SensorMv4, this.o2SensorMv5, this.o2SensorMv6, + this.gf99, + this.n2Load, }); /// Seconds from dive start. @@ -81,6 +84,13 @@ class ProfileSample extends Equatable { final int? o2SensorMv5; final int? o2SensorMv6; + /// Computer-reported GF99, whole percent. Codec v2; null in every v1 blob. + final int? gf99; + + /// Computer-reported aggregate N2 tissue loading, whole percent. Codec v2; + /// null in every v1 blob. + final int? n2Load; + /// The same sample [seconds] later (negative moves it earlier). Merge and /// consolidation re-base a segment's samples onto the combined timeline. ProfileSample shiftedBy(int seconds) => ProfileSample( @@ -112,6 +122,43 @@ class ProfileSample extends Equatable { o2SensorMv4: o2SensorMv4, o2SensorMv5: o2SensorMv5, o2SensorMv6: o2SensorMv6, + gf99: gf99, + n2Load: n2Load, + ); + + /// The same sample with exactly these tissue fields: an omitted argument + /// clears the field, it does not keep it. + ProfileSample withTissue({int? gf99, int? n2Load}) => ProfileSample( + timestamp: timestamp, + depth: depth, + pressure: pressure, + temperature: temperature, + heartRate: heartRate, + ascentRate: ascentRate, + ceiling: ceiling, + ndl: ndl, + setpoint: setpoint, + ppO2: ppO2, + o2Sensor1: o2Sensor1, + o2Sensor2: o2Sensor2, + o2Sensor3: o2Sensor3, + o2Sensor4: o2Sensor4, + o2Sensor5: o2Sensor5, + o2Sensor6: o2Sensor6, + cns: cns, + tts: tts, + rbt: rbt, + decoType: decoType, + heartRateSource: heartRateSource, + heading: heading, + o2SensorMv1: o2SensorMv1, + o2SensorMv2: o2SensorMv2, + o2SensorMv3: o2SensorMv3, + o2SensorMv4: o2SensorMv4, + o2SensorMv5: o2SensorMv5, + o2SensorMv6: o2SensorMv6, + gf99: gf99, + n2Load: n2Load, ); @override @@ -144,5 +191,7 @@ class ProfileSample extends Equatable { o2SensorMv4, o2SensorMv5, o2SensorMv6, + gf99, + n2Load, ]; } diff --git a/lib/features/dive_log/domain/codecs/profile_sample_point.dart b/lib/features/dive_log/domain/codecs/profile_sample_point.dart index 39ebecde2f..254419a3fd 100644 --- a/lib/features/dive_log/domain/codecs/profile_sample_point.dart +++ b/lib/features/dive_log/domain/codecs/profile_sample_point.dart @@ -37,6 +37,8 @@ extension ProfileSampleToPoint on ProfileSample { rbt: rbt, decoType: decoType, tts: tts, + gf99: gf99, + n2Load: n2Load, ); } } @@ -76,5 +78,7 @@ ProfileSample profileSampleFromPoint( o2SensorMv4: point.o2SensorMv4, o2SensorMv5: point.o2SensorMv5, o2SensorMv6: point.o2SensorMv6, + gf99: point.gf99, + n2Load: point.n2Load, ); } diff --git a/lib/features/dive_log/domain/codecs/profile_series_codec.dart b/lib/features/dive_log/domain/codecs/profile_series_codec.dart index a74978c8b8..dedb6bdaad 100644 --- a/lib/features/dive_log/domain/codecs/profile_series_codec.dart +++ b/lib/features/dive_log/domain/codecs/profile_series_codec.dart @@ -32,16 +32,24 @@ class EncodedProfileSeries { /// Versioning: a later codec appends fields under a new version byte. The /// decoder selects the field table by the blob's version byte, so an older /// blob decodes under a newer codec with its missing fields null. A version -/// this codec does not know is refused. +/// this codec does not know is refused (see [UnknownSeriesVersionException] +/// for how the sync door tells a forward version from corruption). +/// +/// v2 appended `gf99` and `n2_load` (issue: computer tissue import). class ProfileSeriesCodec { - const ProfileSeriesCodec({this.fieldTables = const {version: fieldTableV1}}); + const ProfileSeriesCodec({ + this.fieldTables = const {1: fieldTableV1, version: fieldTableV2}, + }); /// The version new blobs are written with. - static const int version = 1; + static const int version = 2; /// Codec v1 field table. See [kProfileFieldTableV1]. static const List fieldTableV1 = kProfileFieldTableV1; + /// Codec v2 field table. See [kProfileFieldTableV2]. + static const List fieldTableV2 = kProfileFieldTableV2; + /// Field table per version byte this codec can read. final Map> fieldTables; @@ -133,8 +141,8 @@ class ProfileSeriesCodec { } // The count sizes one list per field, so it needs a hard cap of its // own: the payload guard below bounds it only by the body, and a body - // at the inflate cap admits a count near 67 million, which for 28 - // columns is about 15 GB of references before any value is read. + // at the inflate cap admits a count near 67 million, which for 30 + // columns is about 16 GB of references before any value is read. if (count > kMaxSeriesSampleCount) { throw ProfileSeriesCodecException( 'sample count $count exceeds the maximum $kMaxSeriesSampleCount', @@ -188,14 +196,14 @@ class ProfileSeriesCodec { throw ArgumentError.value( table, 'fieldTables', - '${field.name} is ${frozen.name} in v1, not ${field.kind.name}', + '${field.name} is frozen as ${frozen.name}, not ${field.kind.name}', ); } } } static final Map _frozenKinds = { - for (final field in kProfileFieldTableV1) field.name: field.kind, + for (final field in kProfileFieldTableV2) field.name: field.kind, }; static Object? _fieldOf(ProfileSample s, String name) => switch (name) { @@ -227,6 +235,8 @@ class ProfileSeriesCodec { 'o2_sensor_mv4' => s.o2SensorMv4, 'o2_sensor_mv5' => s.o2SensorMv5, 'o2_sensor_mv6' => s.o2SensorMv6, + 'gf99' => s.gf99, + 'n2_load' => s.n2Load, _ => throw ArgumentError.value(name, 'name', 'not a profile sample field'), }; @@ -397,6 +407,8 @@ class ProfileSeriesCodec { final mv4s = column('o2_sensor_mv4'); final mv5s = column('o2_sensor_mv5'); final mv6s = column('o2_sensor_mv6'); + final gf99s = column('gf99'); + final n2Loads = column('n2_load'); return [ for (var i = 0; i < count; i++) @@ -433,6 +445,8 @@ class ProfileSeriesCodec { o2SensorMv4: mv4s[i], o2SensorMv5: mv5s[i], o2SensorMv6: mv6s[i], + gf99: gf99s[i], + n2Load: n2Loads[i], ), ]; } diff --git a/lib/features/dive_log/domain/entities/computer_tissue_snapshot.dart b/lib/features/dive_log/domain/entities/computer_tissue_snapshot.dart new file mode 100644 index 0000000000..de3fd42fe9 --- /dev/null +++ b/lib/features/dive_log/domain/entities/computer_tissue_snapshot.dart @@ -0,0 +1,240 @@ +import 'dart:convert'; + +import 'package:equatable/equatable.dart'; + +import 'package:submersion/core/utils/number_utils.dart'; + +/// Tissue state reported by the dive computer itself, as the import found +/// it. Never computed by the app. Any field the source did not carry is null. +/// +/// Stored on `dives.computer_tissue_json` as the JSON [toJson] writes (keys +/// are snake_case). Importers set it on `Dive.computerTissue`; the +/// map-based import pipeline carries it under the dive map key +/// `'computerTissue'` as a [ComputerTissueSnapshot] (a JSON map or JSON text +/// is accepted too, see [from]). +class ComputerTissueSnapshot extends Equatable { + const ComputerTissueSnapshot({this.algorithm, this.start, this.end}); + + /// Raw model name from the source, e.g. 'Suunto Fused2 RGBM', 'zhl_16c', + /// 'buhlmann'. Not normalised. + final String? algorithm; + + /// State at dive start (residual loading from earlier dives). + final ComputerTissueState? start; + + /// State at dive end. + final ComputerTissueState? end; + + /// Whether the end state (or, failing that, the start state) carries + /// per-compartment values. + bool get hasCompartmentData => (end ?? start)?.hasCompartments ?? false; + + ComputerTissueSnapshot copyWith({ + String? algorithm, + ComputerTissueState? start, + ComputerTissueState? end, + }) { + return ComputerTissueSnapshot( + algorithm: algorithm ?? this.algorithm, + start: start ?? this.start, + end: end ?? this.end, + ); + } + + /// JSON object with only the non-null fields. + Map toJson() => { + if (algorithm != null) 'algorithm': algorithm, + if (start != null) 'start': start!.toJson(), + if (end != null) 'end': end!.toJson(), + }; + + /// Reads [toJson]'s shape. Missing keys and malformed values read as + /// null; this never throws on a map. + factory ComputerTissueSnapshot.fromJson(Map json) { + final algorithm = json['algorithm']; + return ComputerTissueSnapshot( + algorithm: algorithm is String ? algorithm : null, + start: _stateOf(json['start']), + end: _stateOf(json['end']), + ); + } + + static ComputerTissueState? _stateOf(Object? value) => + value is Map + ? ComputerTissueState.fromJson(value) + : null; + + /// The JSON text the database column stores. + String encode() => jsonEncode(toJson()); + + /// Reads [encode]'s output. Null and blank text read as null (the column + /// is nullable). Throws [FormatException] when [text] is not a JSON + /// object; malformed values inside the object read as null. + static ComputerTissueSnapshot? decode(String? text) { + if (text == null || text.trim().isEmpty) return null; + final Object? decoded; + try { + decoded = jsonDecode(text); + } on FormatException catch (e) { + throw FormatException('computer tissue JSON is malformed: ${e.message}'); + } + if (decoded is! Map) { + throw const FormatException('computer tissue JSON is not an object'); + } + return ComputerTissueSnapshot.fromJson(decoded); + } + + /// [decode] that answers null instead of throwing. For the storage read + /// path, where one unreadable column must not take the dive with it. + static ComputerTissueSnapshot? tryDecode(String? text) { + try { + return decode(text); + } on FormatException { + return null; + } + } + + /// Coerces what an import map may hold under `'computerTissue'`: a + /// snapshot, a JSON object, or JSON text. Anything else is null. + static ComputerTissueSnapshot? from(Object? value) => switch (value) { + ComputerTissueSnapshot() => value, + Map() => ComputerTissueSnapshot.fromJson(value), + String() => tryDecode(value), + _ => null, + }; + + @override + List get props => [algorithm, start, end]; +} + +/// One instant's tissue state as the computer reported it. Every field is +/// optional; a source fills what it has. +class ComputerTissueState extends Equatable { + const ComputerTissueState({ + this.n2Bar, + this.heBar, + this.loadPercent, + this.n2LoadPercent, + this.gf99Percent, + this.surfaceGfPercent, + this.cnsPercent, + this.otu, + this.rgbmNitrogen, + this.rgbmHelium, + }); + + /// Per-compartment inert N2 tension, bar. + final List? n2Bar; + + /// Per-compartment He tension, bar. + final List? heBar; + + /// Per-compartment loading, percent (Ratio iX3M). + final List? loadPercent; + + /// Aggregate N2 loading, percent (Garmin start_n2 / end_n2). + final double? n2LoadPercent; + + /// GF99 at that instant, percent (Shearwater EndGF99). + final double? gf99Percent; + + /// Surface GF, percent (Shearwater startGFS). + final double? surfaceGfPercent; + + final double? cnsPercent; + final double? otu; + + /// Suunto RGBM factors. + final double? rgbmNitrogen; + final double? rgbmHelium; + + /// Number of compartments, from the tensions first and the loading + /// percentages otherwise; null when neither is present. + int? get compartmentCount => n2Bar?.length ?? loadPercent?.length; + + bool get hasCompartments => (compartmentCount ?? 0) > 0; + + ComputerTissueState copyWith({ + List? n2Bar, + List? heBar, + List? loadPercent, + double? n2LoadPercent, + double? gf99Percent, + double? surfaceGfPercent, + double? cnsPercent, + double? otu, + double? rgbmNitrogen, + double? rgbmHelium, + }) { + return ComputerTissueState( + n2Bar: n2Bar ?? this.n2Bar, + heBar: heBar ?? this.heBar, + loadPercent: loadPercent ?? this.loadPercent, + n2LoadPercent: n2LoadPercent ?? this.n2LoadPercent, + gf99Percent: gf99Percent ?? this.gf99Percent, + surfaceGfPercent: surfaceGfPercent ?? this.surfaceGfPercent, + cnsPercent: cnsPercent ?? this.cnsPercent, + otu: otu ?? this.otu, + rgbmNitrogen: rgbmNitrogen ?? this.rgbmNitrogen, + rgbmHelium: rgbmHelium ?? this.rgbmHelium, + ); + } + + /// JSON object with only the non-null fields, snake_case keys. + Map toJson() => { + if (n2Bar != null) 'n2_bar': n2Bar, + if (heBar != null) 'he_bar': heBar, + if (loadPercent != null) 'load_percent': loadPercent, + if (n2LoadPercent != null) 'n2_load_percent': n2LoadPercent, + if (gf99Percent != null) 'gf99_percent': gf99Percent, + if (surfaceGfPercent != null) 'surface_gf_percent': surfaceGfPercent, + if (cnsPercent != null) 'cns_percent': cnsPercent, + if (otu != null) 'otu': otu, + if (rgbmNitrogen != null) 'rgbm_nitrogen': rgbmNitrogen, + if (rgbmHelium != null) 'rgbm_helium': rgbmHelium, + }; + + /// Reads [toJson]'s shape. A missing key or a non-numeric value reads as + /// null; a list with any non-numeric element reads as null as a whole, + /// because compartment order is positional and a list with a hole is + /// not a shorter list. Never throws. + factory ComputerTissueState.fromJson(Map json) { + return ComputerTissueState( + n2Bar: _doubleListOf(json['n2_bar']), + heBar: _doubleListOf(json['he_bar']), + loadPercent: _doubleListOf(json['load_percent']), + n2LoadPercent: asDoubleOrNull(json['n2_load_percent']), + gf99Percent: asDoubleOrNull(json['gf99_percent']), + surfaceGfPercent: asDoubleOrNull(json['surface_gf_percent']), + cnsPercent: asDoubleOrNull(json['cns_percent']), + otu: asDoubleOrNull(json['otu']), + rgbmNitrogen: asDoubleOrNull(json['rgbm_nitrogen']), + rgbmHelium: asDoubleOrNull(json['rgbm_helium']), + ); + } + + static List? _doubleListOf(Object? value) { + if (value is! List) return null; + final values = []; + for (final element in value) { + final number = asDoubleOrNull(element); + if (number == null) return null; + values.add(number); + } + return List.unmodifiable(values); + } + + @override + List get props => [ + n2Bar, + heBar, + loadPercent, + n2LoadPercent, + gf99Percent, + surfaceGfPercent, + cnsPercent, + otu, + rgbmNitrogen, + rgbmHelium, + ]; +} diff --git a/lib/features/dive_log/domain/entities/dive.dart b/lib/features/dive_log/domain/entities/dive.dart index 4f11a6dd9c..8187177128 100644 --- a/lib/features/dive_log/domain/entities/dive.dart +++ b/lib/features/dive_log/domain/entities/dive.dart @@ -13,6 +13,7 @@ import 'package:submersion/features/equipment/domain/entities/gear_link.dart'; import 'package:submersion/features/equipment/domain/entities/gear_provenance.dart'; import 'package:submersion/features/tags/domain/entities/tag.dart'; import 'package:submersion/features/trips/domain/entities/trip.dart'; +import 'package:submersion/features/dive_log/domain/entities/computer_tissue_snapshot.dart'; import 'package:submersion/features/dive_log/domain/entities/dive_custom_field.dart'; import 'package:submersion/features/dive_log/domain/entities/dive_weight.dart'; import 'package:submersion/features/dive_log/domain/services/bottom_time_calculator.dart'; @@ -106,6 +107,9 @@ class Dive extends Equatable { // Decompression algorithm and conservatism final String? decoAlgorithm; // "buhlmann", "vpm", "rgbm", "dciem" final int? decoConservatism; // Personal adjustment (0=neutral) + // Tissue state the dive computer itself reported, as imported. Never + // computed by the app; null when the source carried none. + final ComputerTissueSnapshot? computerTissue; // Dive computer that logged this dive final String? diveComputerModel; final String? diveComputerSerial; @@ -251,6 +255,7 @@ class Dive extends Equatable { this.gradientFactorHigh, this.decoAlgorithm, this.decoConservatism, + this.computerTissue, this.diveComputerModel, this.diveComputerSerial, this.diveComputerFirmware, @@ -657,6 +662,7 @@ class Dive extends Equatable { int? gradientFactorHigh, String? decoAlgorithm, int? decoConservatism, + ComputerTissueSnapshot? computerTissue, String? diveComputerModel, String? diveComputerSerial, String? diveComputerFirmware, @@ -756,6 +762,7 @@ class Dive extends Equatable { gradientFactorHigh: gradientFactorHigh ?? this.gradientFactorHigh, decoAlgorithm: decoAlgorithm ?? this.decoAlgorithm, decoConservatism: decoConservatism ?? this.decoConservatism, + computerTissue: computerTissue ?? this.computerTissue, diveComputerModel: diveComputerModel ?? this.diveComputerModel, diveComputerSerial: diveComputerSerial ?? this.diveComputerSerial, diveComputerFirmware: diveComputerFirmware ?? this.diveComputerFirmware, @@ -857,6 +864,7 @@ class Dive extends Equatable { gradientFactorHigh, decoAlgorithm, decoConservatism, + computerTissue, diveComputerModel, diveComputerSerial, diveComputerFirmware, @@ -944,6 +952,10 @@ class DiveProfilePoint extends Equatable { final int? rbt; // Remaining Bottom Time in seconds final int? decoType; // 0=NDL, 1=safety stop, 2=deco stop, 3=deep stop final int? tts; // Time To Surface in seconds + // Computer-reported tissue loading, both whole percents; null when the + // source carried none. Never computed by the app. + final int? gf99; // Computer-reported GF99, percent + final int? n2Load; // Computer-reported aggregate N2 tissue loading, percent const DiveProfilePoint({ required this.timestamp, @@ -973,6 +985,8 @@ class DiveProfilePoint extends Equatable { this.rbt, this.decoType, this.tts, + this.gf99, + this.n2Load, }); DiveProfilePoint copyWith({ @@ -1003,6 +1017,8 @@ class DiveProfilePoint extends Equatable { int? rbt, int? decoType, int? tts, + int? gf99, + int? n2Load, }) { return DiveProfilePoint( timestamp: timestamp ?? this.timestamp, @@ -1032,6 +1048,8 @@ class DiveProfilePoint extends Equatable { rbt: rbt ?? this.rbt, decoType: decoType ?? this.decoType, tts: tts ?? this.tts, + gf99: gf99 ?? this.gf99, + n2Load: n2Load ?? this.n2Load, ); } @@ -1064,6 +1082,8 @@ class DiveProfilePoint extends Equatable { rbt, decoType, tts, + gf99, + n2Load, ]; } diff --git a/lib/features/dive_log/domain/services/dive_merge_builder.dart b/lib/features/dive_log/domain/services/dive_merge_builder.dart index fe031700dc..0c3047712a 100644 --- a/lib/features/dive_log/domain/services/dive_merge_builder.dart +++ b/lib/features/dive_log/domain/services/dive_merge_builder.dart @@ -352,6 +352,7 @@ class DiveMergeBuilder { gradientFactorHigh: _firstNonNull(sorted, (d) => d.gradientFactorHigh), decoAlgorithm: _firstNonNull(sorted, (d) => d.decoAlgorithm), decoConservatism: _firstNonNull(sorted, (d) => d.decoConservatism), + computerTissue: _firstNonNull(sorted, (d) => d.computerTissue), diveComputerModel: _firstNonNull(sorted, (d) => d.diveComputerModel), diveComputerSerial: _firstNonNull(sorted, (d) => d.diveComputerSerial), diveComputerFirmware: _firstNonNull( diff --git a/lib/features/dive_log/presentation/pages/dive_detail_page.dart b/lib/features/dive_log/presentation/pages/dive_detail_page.dart index d6bd27aea5..aca29162c5 100644 --- a/lib/features/dive_log/presentation/pages/dive_detail_page.dart +++ b/lib/features/dive_log/presentation/pages/dive_detail_page.dart @@ -2319,6 +2319,8 @@ class _DiveDetailPageState extends ConsumerState { selectedIndex: selectedPointIndex, subtitle: timeSubtitle, expandVisualization: expand, + computerTissue: dive.computerTissue, + calculatedCnsPercent: analysis.o2Exposure.cnsEnd, onHeatMapHover: (index) { ref.read(profileTrackingIndexProvider(diveId).notifier).state = index; }, diff --git a/lib/features/dive_log/presentation/pages/dive_edit_page.dart b/lib/features/dive_log/presentation/pages/dive_edit_page.dart index 8bb29862cf..0a7c0c9e11 100644 --- a/lib/features/dive_log/presentation/pages/dive_edit_page.dart +++ b/lib/features/dive_log/presentation/pages/dive_edit_page.dart @@ -5360,6 +5360,7 @@ class _DiveEditPageState extends ConsumerState { decoConservatism: _existingDive?.decoConservatism, gradientFactorLow: _existingDive?.gradientFactorLow, gradientFactorHigh: _existingDive?.gradientFactorHigh, + computerTissue: _existingDive?.computerTissue, weatherCode: _existingDive?.weatherCode, importId: _existingDive?.importId, surfaceInterval: _existingDive?.surfaceInterval, diff --git a/lib/features/dive_log/presentation/providers/computer_gf99_overlay.dart b/lib/features/dive_log/presentation/providers/computer_gf99_overlay.dart new file mode 100644 index 0000000000..e0bd9f2726 --- /dev/null +++ b/lib/features/dive_log/presentation/providers/computer_gf99_overlay.dart @@ -0,0 +1,44 @@ +import 'package:submersion/features/dive_log/domain/entities/dive.dart'; + +/// Series built from the tissue values a dive computer logs per sample: +/// GF99 ([DiveProfilePoint.gf99]) and aggregate N2 loading +/// ([DiveProfilePoint.n2Load]). +/// +/// Nothing imported carries per-sample per-compartment tensions, so these +/// only ever replace or sit beside the app's own Buhlmann curves; the +/// 16-compartment recompute keeps running underneath. + +/// Whether any sample carries a computer-reported GF99. +/// +/// Unlike NDL/ceiling/TTS a zero is a real reading here (fresh tissues at +/// the start of a dive read 0), so presence is null-based, as for CNS. +bool hasComputerGf99(List profile) => + profile.any((p) => p.gf99 != null); + +/// The GF99 curve with the computer's value wherever it reported one and the +/// calculated value where it did not, one entry per sample. Mirrors the NDL +/// overlay's fallback: a sample past the end of [calculated] reads as 0. +List buildComputerGf99Curve( + List profile, + List? calculated, +) { + return List.generate(profile.length, (i) { + final computer = profile[i].gf99; + if (computer != null) return computer.toDouble(); + if (calculated != null && i < calculated.length) return calculated[i]; + return 0.0; + }); +} + +/// Whether any sample carries a computer-reported aggregate N2 loading. +bool hasComputerN2Load(List profile) => + profile.any((p) => p.n2Load != null); + +/// The computer's aggregate N2 loading verbatim, one entry per sample with +/// null where it reported nothing. Null when no sample carries it. This is +/// a computer-only metric with no calculated counterpart, so gaps stay gaps +/// rather than borrowing a value from elsewhere. +List? buildComputerN2LoadCurve(List profile) { + if (!hasComputerN2Load(profile)) return null; + return List.generate(profile.length, (i) => profile[i].n2Load); +} diff --git a/lib/features/dive_log/presentation/providers/profile_analysis_provider.dart b/lib/features/dive_log/presentation/providers/profile_analysis_provider.dart index e9b30db39e..ef6c8f414a 100644 --- a/lib/features/dive_log/presentation/providers/profile_analysis_provider.dart +++ b/lib/features/dive_log/presentation/providers/profile_analysis_provider.dart @@ -25,6 +25,7 @@ import 'package:submersion/features/dive_log/domain/entities/profile_event.dart' import 'package:submersion/features/dive_log/domain/services/computer_cns_extractor.dart'; import 'package:submersion/features/dive_log/domain/services/gas_time_remaining.dart'; import 'package:submersion/features/dive_log/domain/services/profile_event_mapper.dart'; +import 'package:submersion/features/dive_log/presentation/providers/computer_gf99_overlay.dart'; import 'package:submersion/features/dive_log/presentation/providers/dive_computer_providers.dart'; import 'package:submersion/features/dive_log/presentation/providers/dive_providers.dart'; import 'package:submersion/features/dive_log/presentation/providers/profile_legend_provider.dart'; @@ -428,12 +429,20 @@ ProfileAnalysisService _resolveAnalysisService( /// Overlays computer-reported decompression data onto a calculated /// [ProfileAnalysis]. /// -/// Each metric (NDL, ceiling, TTS, CNS, deco stop band) is independently +/// Each metric (NDL, ceiling, TTS, CNS, deco stop band, GF99) is independently /// controlled by its own [MetricDataSource] parameter. When a source is /// [MetricDataSource.computer] and computer data exists in the profile, /// those values take priority over the Buhlmann-calculated values. Points /// without computer data fall back to the calculated values. /// +/// [gf99Source] alone defaults to computer: where the computer logged its +/// GF99 the diver saw that number, and the app's Buhlmann GF99 is the +/// fallback for computers that do not log one. There is no legend toggle or +/// diver setting for it (each per-metric default source is its own +/// diver_settings column). The computer's aggregate N2 loading, which has no +/// calculated counterpart, is always exposed as +/// [ProfileAnalysis.n2LoadCurve] when present. +/// /// The deco stop band ([decoStopSource]) resolves against the incoming /// (calculated) [ProfileAnalysis.decoStopCurve] rather than against the /// possibly-overlaid ceiling curve, so choosing computer data for the @@ -451,6 +460,7 @@ ProfileAnalysisService _resolveAnalysisService( MetricDataSource cnsSource = MetricDataSource.calculated, MetricDataSource decoStopSource = MetricDataSource.calculated, MetricDataSource gtrSource = MetricDataSource.calculated, + MetricDataSource gf99Source = MetricDataSource.computer, RebreatherPpO2? rebreatherPpO2, }) { // A zero is not a reading. Computers that do not measure one of these @@ -469,6 +479,7 @@ ProfileAnalysisService _resolveAnalysisService( // Air-integrated computers log their own GTR (libdc RBT, stored in // seconds); a null sample is the computer blanking its display. final hasComputerGtr = profile.any((p) => p.rbt != null); + final hasGf99 = hasComputerGf99(profile); final useNdl = ndlSource == MetricDataSource.computer && hasComputerNdl; final useCeiling = @@ -476,6 +487,9 @@ ProfileAnalysisService _resolveAnalysisService( final useTts = ttsSource == MetricDataSource.computer && hasComputerTts; final useCns = cnsSource == MetricDataSource.computer && hasComputerCns; final useGtr = gtrSource == MetricDataSource.computer && hasComputerGtr; + final useGf99 = gf99Source == MetricDataSource.computer && hasGf99; + // Computer-only: nothing to choose between, so no source parameter. + final n2LoadCurve = buildComputerN2LoadCurve(profile); // Resolved independently of useCeiling: the deco stop band must not be // dragged along when the user picks "computer" for the ceiling line alone. final useDecoStop = @@ -505,6 +519,9 @@ ProfileAnalysisService _resolveAnalysisService( ? MetricDataSource.computer : MetricDataSource.calculated, gtrActual: useGtr ? MetricDataSource.computer : MetricDataSource.calculated, + gf99Actual: useGf99 + ? MetricDataSource.computer + : MetricDataSource.calculated, ); if (!useNdl && @@ -513,11 +530,19 @@ ProfileAnalysisService _resolveAnalysisService( !useTts && !useCns && !useGtr && + !useGf99 && resolvedPpO2 == null) { - // Millivolts stand alone: a dive can carry them with no ppO2, cells or - // setpoint to overlay, and they must not be dropped on this path (#810). - if (o2CellMvCurves != null) { - return (analysis.copyWith(o2CellMvCurves: o2CellMvCurves), sourceInfo); + // Millivolts and N2 loading stand alone: a dive can carry them with no + // ppO2, cells or setpoint to overlay, and they must not be dropped on + // this path (#810). + if (o2CellMvCurves != null || n2LoadCurve != null) { + return ( + analysis.copyWith( + o2CellMvCurves: o2CellMvCurves, + n2LoadCurve: n2LoadCurve, + ), + sourceInfo, + ); } return (analysis, sourceInfo); } @@ -576,6 +601,8 @@ ProfileAnalysisService _resolveAnalysisService( gtrCurve: useGtr ? List.generate(profile.length, (i) => profile[i].rbt) : null, + gfCurve: useGf99 ? buildComputerGf99Curve(profile, analysis.gfCurve) : null, + n2LoadCurve: n2LoadCurve, // ppO2 from sensor/setpoint (null keeps the calculated curve). ppO2Curve: resolvedPpO2, o2SensorCurves: o2SensorCurves, @@ -1182,6 +1209,9 @@ Future computeAnalysisForProfile( cnsSource: cnsSource, decoStopSource: decoStopSource, gtrSource: gtrSource, + // Computer-first by product rule; no legend toggle or setting exists + // for GF99 (see overlayComputerDecoData). + gf99Source: MetricDataSource.computer, rebreatherPpO2: rebreatherPpO2, ); @@ -1712,6 +1742,7 @@ final diveProfileAnalysisProvider = Provider.family(( cnsSource: MetricDataSource.computer, decoStopSource: MetricDataSource.computer, gtrSource: MetricDataSource.computer, + gf99Source: MetricDataSource.computer, rebreatherPpO2: rebreatherPpO2, ); return overlaid; diff --git a/lib/features/dive_log/presentation/widgets/compact_tissue_loading_card.dart b/lib/features/dive_log/presentation/widgets/compact_tissue_loading_card.dart index c9a8148d01..8078ba7b92 100644 --- a/lib/features/dive_log/presentation/widgets/compact_tissue_loading_card.dart +++ b/lib/features/dive_log/presentation/widgets/compact_tissue_loading_card.dart @@ -4,6 +4,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:submersion/core/accessibility/semantic_helpers.dart'; import 'package:submersion/core/deco/entities/deco_status.dart'; import 'package:submersion/core/deco/entities/tissue_compartment.dart'; +import 'package:submersion/features/dive_log/domain/entities/computer_tissue_snapshot.dart'; +import 'package:submersion/features/dive_log/presentation/widgets/computer_tissue_section.dart'; import 'package:submersion/features/dive_log/presentation/widgets/tissue_area_chart.dart'; import 'package:submersion/features/dive_log/presentation/widgets/tissue_color_schemes.dart'; import 'package:submersion/features/dive_log/presentation/widgets/tissue_heat_map.dart'; @@ -60,6 +62,14 @@ class CompactTissueLoadingCard extends ConsumerStatefulWidget { /// keeping this widget free of any dive_3d navigation dependency. final VoidCallback? onOpen3dView; + /// Tissue state the dive computer itself reported, shown in its own + /// section under the calculated detail when present. + final ComputerTissueSnapshot? computerTissue; + + /// End-of-dive calculated CNS%, the secondary line under the computer's + /// CNS in [computerTissue]. Unused without a snapshot. + final double? calculatedCnsPercent; + const CompactTissueLoadingCard({ super.key, required this.status, @@ -69,6 +79,8 @@ class CompactTissueLoadingCard extends ConsumerStatefulWidget { this.onHeatMapHover, this.expandVisualization = false, this.onOpen3dView, + this.computerTissue, + this.calculatedCnsPercent, }); @override @@ -169,6 +181,12 @@ class _CompactTissueLoadingCardState const SizedBox(height: 6), ], + // What the dive computer itself reported, when the import had it + if (widget.computerTissue != null) ...[ + _buildComputerTissueSection(), + const SizedBox(height: 6), + ], + // Tissue pressure bar chart (with per-bar hover) _buildTissuePressureDiagram(context, colorScheme), const SizedBox(height: 6), @@ -182,6 +200,23 @@ class _CompactTissueLoadingCardState ); } + /// The computer's values lead; the app's end-of-dive numbers follow as the + /// secondary line where both exist. Compares against the final status, not + /// the hovered one, because the snapshot is end-of-dive. + Widget _buildComputerTissueSection() { + final statuses = widget.decoStatuses; + final endStatus = statuses != null && statuses.isNotEmpty + ? statuses.last + : widget.status; + final hasCalculated = endStatus.compartments.isNotEmpty; + return ComputerTissueSection( + snapshot: widget.computerTissue, + calculatedGf99Percent: hasCalculated ? endStatus.gf99 : null, + calculatedSurfaceGfPercent: hasCalculated ? endStatus.surfGf : null, + calculatedCnsPercent: widget.calculatedCnsPercent, + ); + } + Widget _buildTissuePressureDiagram( BuildContext context, ColorScheme colorScheme, diff --git a/lib/features/dive_log/presentation/widgets/computer_tissue_formatter.dart b/lib/features/dive_log/presentation/widgets/computer_tissue_formatter.dart new file mode 100644 index 0000000000..fd192932a0 --- /dev/null +++ b/lib/features/dive_log/presentation/widgets/computer_tissue_formatter.dart @@ -0,0 +1,69 @@ +import 'package:submersion/core/constants/units.dart'; +import 'package:submersion/core/utils/number_display.dart'; +import 'package:submersion/core/utils/unit_formatter.dart'; +import 'package:submersion/features/dive_log/domain/entities/computer_tissue_snapshot.dart'; + +/// Formatting for the dive computer's own tissue values. Pure functions so +/// the section widget only lays things out. + +/// Placeholder for a start or end value the computer did not report. +const computerTissueMissing = '--'; + +/// Whole-percent text, e.g. `82%`. +String formatTissuePercent(double value) => + '${formatFixedForDisplay(value, 0)}%'; + +/// OTU is a count; shown whole. +String formatTissueOtu(double value) => formatFixedForDisplay(value, 0); + +/// Suunto RGBM factors are unitless multipliers around 1. +String formatRgbmFactor(double value) => formatFixedForDisplay(value, 2); + +/// A compartment tension in the diver's pressure unit. Tensions sit around +/// 1 to 4 bar, so bar needs two decimals to tell compartments apart and psi +/// needs one. +String formatTissueTension(UnitFormatter units, double bar) { + final decimals = units.settings.pressureUnit == PressureUnit.bar ? 2 : 1; + return units.formatPressure(bar, decimals: decimals); +} + +/// Aggregate metrics the snapshot's start/end table can carry. +enum ComputerTissueMetric { n2Load, gf99, surfaceGf, cns, otu } + +/// One row of the start/end table, values already formatted; null where +/// that instant did not carry the metric. +typedef ComputerTissueAggregateRow = ({ + ComputerTissueMetric metric, + String? start, + String? end, +}); + +/// The rows of the start/end table, in display order, keeping only metrics +/// at least one instant reported. +List aggregateRows( + ComputerTissueSnapshot snapshot, +) { + final rows = []; + for (final metric in ComputerTissueMetric.values) { + final start = _format(metric, snapshot.start); + final end = _format(metric, snapshot.end); + if (start == null && end == null) continue; + rows.add((metric: metric, start: start, end: end)); + } + return List.unmodifiable(rows); +} + +String? _format(ComputerTissueMetric metric, ComputerTissueState? state) { + if (state == null) return null; + final value = switch (metric) { + ComputerTissueMetric.n2Load => state.n2LoadPercent, + ComputerTissueMetric.gf99 => state.gf99Percent, + ComputerTissueMetric.surfaceGf => state.surfaceGfPercent, + ComputerTissueMetric.cns => state.cnsPercent, + ComputerTissueMetric.otu => state.otu, + }; + if (value == null) return null; + return metric == ComputerTissueMetric.otu + ? formatTissueOtu(value) + : formatTissuePercent(value); +} diff --git a/lib/features/dive_log/presentation/widgets/computer_tissue_section.dart b/lib/features/dive_log/presentation/widgets/computer_tissue_section.dart new file mode 100644 index 0000000000..a54ae51f1d --- /dev/null +++ b/lib/features/dive_log/presentation/widgets/computer_tissue_section.dart @@ -0,0 +1,394 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'package:submersion/core/utils/unit_formatter.dart'; +import 'package:submersion/features/dive_log/domain/entities/computer_tissue_snapshot.dart'; +import 'package:submersion/features/dive_log/presentation/widgets/computer_tissue_formatter.dart'; +import 'package:submersion/features/settings/presentation/providers/settings_providers.dart'; +import 'package:submersion/l10n/arb/app_localizations.dart'; +import 'package:submersion/l10n/l10n_extension.dart'; + +/// The tissue state the dive computer itself reported for a dive, shown +/// under the app's own tissue loading. Renders nothing without a snapshot. +/// +/// Where the computer reported a value the app also computes (end-of-dive +/// GF99, surface GF, CNS) the computer's number takes the row and the +/// calculated one follows as a secondary line, so the diver sees what the +/// computer said first and the app's opinion second. +class ComputerTissueSection extends ConsumerWidget { + const ComputerTissueSection({ + super.key, + required this.snapshot, + this.calculatedGf99Percent, + this.calculatedSurfaceGfPercent, + this.calculatedCnsPercent, + }); + + final ComputerTissueSnapshot? snapshot; + + /// End-of-dive values from the app's Buhlmann recompute, shown as the + /// secondary line under the computer's own where both exist. + final double? calculatedGf99Percent; + final double? calculatedSurfaceGfPercent; + final double? calculatedCnsPercent; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final snapshot = this.snapshot; + if (snapshot == null) return const SizedBox.shrink(); + + final l10n = context.l10n; + final theme = Theme.of(context); + final textTheme = theme.textTheme; + final colorScheme = theme.colorScheme; + final units = UnitFormatter(ref.watch(settingsProvider)); + final end = snapshot.end; + final rows = aggregateRows(snapshot); + final rgbm = _rgbmRows(snapshot, l10n); + + final labelStyle = textTheme.labelSmall?.copyWith( + fontSize: 11, + color: colorScheme.onSurfaceVariant, + ); + final valueStyle = textTheme.labelSmall?.copyWith( + fontSize: 12, + fontWeight: FontWeight.bold, + ); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Divider(height: 1), + const SizedBox(height: 6), + Text( + l10n.diveLog_computerTissue_title, + style: textTheme.labelSmall?.copyWith( + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + if (snapshot.algorithm != null) ...[ + const SizedBox(height: 4), + _LabelValueRow( + label: l10n.diveLog_computerTissue_algorithm, + value: snapshot.algorithm!, + labelStyle: labelStyle, + valueStyle: valueStyle, + ), + ], + if (end?.loadPercent != null) ...[ + const SizedBox(height: 6), + Text(l10n.diveLog_computerTissue_endLoading, style: labelStyle), + const SizedBox(height: 4), + for (var i = 0; i < end!.loadPercent!.length; i++) + ComputerTissueLoadBar( + label: l10n.diveLog_computerTissue_compartment(i + 1), + percent: end.loadPercent![i], + labelStyle: labelStyle, + valueStyle: valueStyle, + ), + _compartmentCount( + l10n, + snapshot, + end.loadPercent!.length, + labelStyle, + ), + ] else if (end?.n2Bar != null) ...[ + const SizedBox(height: 6), + Text(l10n.diveLog_computerTissue_endTensions, style: labelStyle), + const SizedBox(height: 4), + for (var i = 0; i < end!.n2Bar!.length; i++) + _TensionRow( + label: l10n.diveLog_computerTissue_compartment(i + 1), + n2: formatTissueTension(units, end.n2Bar![i]), + he: end.heBar != null && i < end.heBar!.length + ? formatTissueTension(units, end.heBar![i]) + : null, + labelStyle: labelStyle, + valueStyle: valueStyle, + ), + _compartmentCount(l10n, snapshot, end.n2Bar!.length, labelStyle), + ], + if (rows.isNotEmpty) ...[ + const SizedBox(height: 6), + _AggregateTable( + rows: rows, + calculated: { + ComputerTissueMetric.gf99: calculatedGf99Percent, + ComputerTissueMetric.surfaceGf: calculatedSurfaceGfPercent, + ComputerTissueMetric.cns: calculatedCnsPercent, + }, + labelStyle: labelStyle, + valueStyle: valueStyle, + ), + ], + for (final (label, value) in rgbm) ...[ + const SizedBox(height: 4), + _LabelValueRow( + label: label, + value: value, + labelStyle: labelStyle, + valueStyle: valueStyle, + ), + ], + ], + ); + } + + Widget _compartmentCount( + AppLocalizations l10n, + ComputerTissueSnapshot snapshot, + int count, + TextStyle? style, + ) { + final algorithm = snapshot.algorithm; + final text = algorithm == null + ? l10n.diveLog_computerTissue_compartments(count) + : l10n.diveLog_computerTissue_compartmentsWithAlgorithm( + count, + algorithm, + ); + return Padding( + padding: const EdgeInsets.only(top: 2), + child: Text(text, style: style), + ); + } + + List<(String, String)> _rgbmRows( + ComputerTissueSnapshot snapshot, + AppLocalizations l10n, + ) { + // The factors are a property of the whole dive; the end state carries + // them, falling back to the start for a source that only filled that. + final state = snapshot.end ?? snapshot.start; + final n2 = state?.rgbmNitrogen ?? snapshot.start?.rgbmNitrogen; + final he = state?.rgbmHelium ?? snapshot.start?.rgbmHelium; + return [ + if (n2 != null) + (l10n.diveLog_computerTissue_rgbmNitrogen, formatRgbmFactor(n2)), + if (he != null) + (l10n.diveLog_computerTissue_rgbmHelium, formatRgbmFactor(he)), + ]; + } +} + +/// One compartment's loading as a labelled horizontal bar. Public so tests +/// can count compartments by widget type. +class ComputerTissueLoadBar extends StatelessWidget { + const ComputerTissueLoadBar({ + super.key, + required this.label, + required this.percent, + this.labelStyle, + this.valueStyle, + }); + + final String label; + final double percent; + final TextStyle? labelStyle; + final TextStyle? valueStyle; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final fraction = (percent / 100).clamp(0.0, 1.0); + return Padding( + padding: const EdgeInsets.symmetric(vertical: 1), + child: Row( + children: [ + SizedBox(width: 28, child: Text(label, style: labelStyle)), + Expanded( + child: ClipRRect( + borderRadius: BorderRadius.circular(2), + child: SizedBox( + height: 6, + child: Stack( + children: [ + Container(color: colorScheme.surfaceContainerHighest), + FractionallySizedBox( + widthFactor: fraction, + child: Container(color: _loadColor(percent)), + ), + ], + ), + ), + ), + ), + const SizedBox(width: 6), + SizedBox( + width: 36, + child: Text( + formatTissuePercent(percent), + style: valueStyle, + textAlign: TextAlign.end, + ), + ), + ], + ), + ); + } + + static Color _loadColor(double percent) { + if (percent >= 100) return Colors.red; + if (percent >= 80) return Colors.orange; + if (percent >= 60) return Colors.amber; + return Colors.green; + } +} + +class _TensionRow extends StatelessWidget { + const _TensionRow({ + required this.label, + required this.n2, + required this.he, + this.labelStyle, + this.valueStyle, + }); + + final String label; + final String n2; + final String? he; + final TextStyle? labelStyle; + final TextStyle? valueStyle; + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 1), + child: Row( + children: [ + SizedBox(width: 28, child: Text(label, style: labelStyle)), + Text(l10n.diveLog_computerTissue_labelN2, style: labelStyle), + const SizedBox(width: 4), + Text(n2, style: valueStyle), + if (he != null) ...[ + const SizedBox(width: 10), + Text(l10n.diveLog_computerTissue_labelHe, style: labelStyle), + const SizedBox(width: 4), + Text(he!, style: valueStyle), + ], + ], + ), + ); + } +} + +class _LabelValueRow extends StatelessWidget { + const _LabelValueRow({ + required this.label, + required this.value, + this.labelStyle, + this.valueStyle, + }); + + final String label; + final String value; + final TextStyle? labelStyle; + final TextStyle? valueStyle; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded(child: Text(label, style: labelStyle)), + Text(value, style: valueStyle), + ], + ); + } +} + +/// Start vs end mini table. The computer's numbers fill the cells; where the +/// app computed the same end-of-dive metric its value follows on a +/// secondary line beneath the computer's. +class _AggregateTable extends StatelessWidget { + const _AggregateTable({ + required this.rows, + required this.calculated, + this.labelStyle, + this.valueStyle, + }); + + final List rows; + final Map calculated; + final TextStyle? labelStyle; + final TextStyle? valueStyle; + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final secondaryStyle = labelStyle?.copyWith(fontSize: 10); + return Table( + columnWidths: const { + 0: FlexColumnWidth(2), + 1: FlexColumnWidth(1), + 2: FlexColumnWidth(1), + }, + defaultVerticalAlignment: TableCellVerticalAlignment.top, + children: [ + TableRow( + children: [ + const SizedBox.shrink(), + Text( + l10n.diveLog_computerTissue_columnStart, + style: labelStyle, + textAlign: TextAlign.end, + ), + Text( + l10n.diveLog_computerTissue_columnEnd, + style: labelStyle, + textAlign: TextAlign.end, + ), + ], + ), + for (final row in rows) + TableRow( + children: [ + Padding( + padding: const EdgeInsets.only(top: 2), + child: Text(_metricLabel(l10n, row.metric), style: labelStyle), + ), + Padding( + padding: const EdgeInsets.only(top: 2), + child: Text( + row.start ?? computerTissueMissing, + style: valueStyle, + textAlign: TextAlign.end, + ), + ), + Padding( + padding: const EdgeInsets.only(top: 2), + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + row.end ?? computerTissueMissing, + style: valueStyle, + textAlign: TextAlign.end, + ), + if (row.end != null && calculated[row.metric] != null) + Text( + l10n.diveLog_computerTissue_calculated( + formatTissuePercent(calculated[row.metric]!), + ), + style: secondaryStyle, + textAlign: TextAlign.end, + ), + ], + ), + ), + ], + ), + ], + ); + } + + static String _metricLabel(AppLocalizations l10n, ComputerTissueMetric m) => + switch (m) { + ComputerTissueMetric.n2Load => l10n.diveLog_computerTissue_rowN2Load, + ComputerTissueMetric.gf99 => l10n.diveLog_deco_label_gf99, + ComputerTissueMetric.surfaceGf => l10n.diveLog_deco_label_surfGf, + ComputerTissueMetric.cns => l10n.diveLog_computerTissue_rowCns, + ComputerTissueMetric.otu => l10n.diveLog_computerTissue_rowOtu, + }; +} diff --git a/lib/features/universal_import/data/parsers/fit_import_parser.dart b/lib/features/universal_import/data/parsers/fit_import_parser.dart index ef1745e330..9d16a1e9e6 100644 --- a/lib/features/universal_import/data/parsers/fit_import_parser.dart +++ b/lib/features/universal_import/data/parsers/fit_import_parser.dart @@ -82,6 +82,9 @@ class FitImportParser implements ImportParser { if (dive.gfHigh != null) diveData['gradientFactorHigh'] = dive.gfHigh; if (dive.cnsEnd != null) diveData['cnsEnd'] = dive.cnsEnd; if (dive.otu != null) diveData['otu'] = dive.otu; + if (dive.computerTissue != null) { + diveData['computerTissue'] = dive.computerTissue; + } if (dive.computerModel != null) { diveData['diveComputerModel'] = dive.computerModel; } @@ -147,6 +150,7 @@ class FitImportParser implements ImportParser { if (s.temperature != null) point['temperature'] = s.temperature; if (s.heartRate != null) point['heartRate'] = s.heartRate; if (s.cns != null) point['cns'] = s.cns; + if (s.n2Load != null) point['n2Load'] = s.n2Load; if (s.ndlSeconds != null) point['ndl'] = s.ndlSeconds; if (s.ttsSeconds != null) point['tts'] = s.ttsSeconds; if (s.ceiling != null) point['ceiling'] = s.ceiling; diff --git a/lib/features/universal_import/data/parsers/ratio_xml_parser.dart b/lib/features/universal_import/data/parsers/ratio_xml_parser.dart index 61bcaf8ebe..aa6c7250c8 100644 --- a/lib/features/universal_import/data/parsers/ratio_xml_parser.dart +++ b/lib/features/universal_import/data/parsers/ratio_xml_parser.dart @@ -3,6 +3,7 @@ import 'dart:typed_data'; import 'package:xml/xml.dart'; +import 'package:submersion/features/dive_log/domain/entities/computer_tissue_snapshot.dart'; import 'package:submersion/features/dive_log/domain/entities/dive.dart' show GasMix; import 'package:submersion/features/universal_import/data/models/import_enums.dart'; @@ -219,6 +220,8 @@ class RatioXmlParser implements ImportParser { int? lastOtu; int? firstGfLow; int? firstGfHigh; + _TissueReading? firstTissue; + _TissueReading? lastTissue; for (final sample in sampleElements) { final runtimeS = _intElement(sample, 'runtimeS'); @@ -226,6 +229,13 @@ class RatioXmlParser implements ImportParser { lastRuntimeS = runtimeS; + // Per-compartment tissue loading (tissueGroup1Percent ...) + final tissue = _TissueReading.fromSample(sample); + if (tissue != null) { + firstTissue ??= tissue; + lastTissue = tissue; + } + // Depth: decimeters -> meters final depthDm = _intElement(sample, 'depthDm'); final depthM = depthDm != null ? depthDm / 10.0 : 0.0; @@ -376,6 +386,16 @@ class RatioXmlParser implements ImportParser { } } + // Tissue state the computer reported: first sample as the dive start, + // last sample as the dive end. + if (firstTissue != null && lastTissue != null) { + diveData['computerTissue'] = ComputerTissueSnapshot( + algorithm: diveData['decoAlgorithm'] as String?, + start: firstTissue.toState(), + end: lastTissue.toState(), + ); + } + // Build tank data if (tankIdToIndex.isNotEmpty) { _buildTanks( @@ -505,6 +525,37 @@ class RatioXmlParser implements ImportParser { }; } +/// One sample's per-compartment tissue loading, as `` +/// elements numbered from 1, with the CNS the same sample reported. +class _TissueReading { + const _TissueReading({required this.loadPercent, this.cnsPercent}); + + final List loadPercent; + final double? cnsPercent; + + /// Reads groups 1, 2, ... until the first missing or non-numeric one. + /// Null when group 1 is absent or unreadable. + static _TissueReading? fromSample(XmlElement sample) { + final values = []; + for (var group = 1; ; group++) { + final text = sample.getElement('tissueGroup${group}Percent')?.innerText; + final value = text == null ? null : double.tryParse(text.trim()); + if (value == null) break; + values.add(value); + } + if (values.isEmpty) return null; + + final cns = RatioXmlParser._intElement(sample, 'CNS'); + return _TissueReading( + loadPercent: List.unmodifiable(values), + cnsPercent: cns?.toDouble(), + ); + } + + ComputerTissueState toState() => + ComputerTissueState(loadPercent: loadPercent, cnsPercent: cnsPercent); +} + /// Internal helper to track tank pressure readings across samples. class _TankReading { final int runtimeS; diff --git a/lib/features/universal_import/data/services/parsed_dive_profile_mapper.dart b/lib/features/universal_import/data/services/parsed_dive_profile_mapper.dart index 393838dbe9..774675c01a 100644 --- a/lib/features/universal_import/data/services/parsed_dive_profile_mapper.dart +++ b/lib/features/universal_import/data/services/parsed_dive_profile_mapper.dart @@ -14,6 +14,15 @@ class ParsedDiveProfileMapper { /// Builds the `profile` value: one map per sample, with every sensor /// channel libdivecomputer reported. + /// + /// Per-sample tissue loading is NOT set here: libdivecomputer does not + /// report it. Parsers that read it from the file itself (Garmin FIT + /// `n2_load`, Shearwater Cloud `gf99`, UDDF ``) add the + /// keys `'gf99'` and `'n2Load'` to the same sample map, both as whole + /// percents (`int`), and the importer carries them to + /// `DiveProfilePoint.gf99` / `DiveProfilePoint.n2Load`. Dive-level tissue + /// state goes under the dive map key `'computerTissue'` as a + /// `ComputerTissueSnapshot`. static List> samples(pigeon.ParsedDive parsed) { return parsed.samples.map((s) { final sampleMap = { diff --git a/lib/features/universal_import/data/services/shearwater_db_reader.dart b/lib/features/universal_import/data/services/shearwater_db_reader.dart index e98d4f3cf0..d09e6173e1 100644 --- a/lib/features/universal_import/data/services/shearwater_db_reader.dart +++ b/lib/features/universal_import/data/services/shearwater_db_reader.dart @@ -4,11 +4,36 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:sqlite3/sqlite3.dart'; +/// One GF99 reading from `dive_log_records`, in seconds from the start of +/// the dive and whole percent as the computer logged it. +class ShearwaterGf99Sample { + final int timeSeconds; + final int gf99; + + const ShearwaterGf99Sample({required this.timeSeconds, required this.gf99}); + + @override + bool operator ==(Object other) => + other is ShearwaterGf99Sample && + other.timeSeconds == timeSeconds && + other.gf99 == gf99; + + @override + int get hashCode => Object.hash(timeSeconds, gf99); + + @override + String toString() => 'ShearwaterGf99Sample($timeSeconds s, $gf99%)'; +} + /// Raw dive data read directly from a Shearwater Cloud SQLite database. /// /// Fields map 1:1 to columns from the dive_details and log_data tables. /// Binary log data is decompressed; JSON string columns are pre-parsed. /// Empty strings from the database are normalized to null. +/// +/// The computer-tissue fields ([gf99Samples], [startGFS], [gfMin], [gfMax], +/// [decoModel], [startCNS], [endCNS]) come from the optional `dive_logs` and +/// `dive_log_records` tables; older exports lack them and read as empty/null. class ShearwaterRawDive { final String diveId; final String? diveDate; @@ -47,6 +72,25 @@ class ShearwaterRawDive { final Map? headerJson; final Map? footerJson; + /// Per-sample GF99 from `dive_log_records`, ordered by time. + final List gf99Samples; + + /// Surface GF at the start of the dive (`dive_logs.startGFS`), percent. + final double? startGFS; + + /// Gradient factors (`dive_logs.gfMin` / `gfMax`), percent. + final int? gfMin; + final int? gfMax; + + /// Deco model as Shearwater spells it ('GF', 'VPM-B', 'VPM-B/GFS', + /// 'DCIEM'); null when absent or unrecognised. + final String? decoModel; + + /// CNS at the start and end of the dive (`dive_logs.startCNS` / `endCNS`), + /// percent. + final double? startCNS; + final double? endCNS; + const ShearwaterRawDive({ required this.diveId, this.diveDate, @@ -84,6 +128,13 @@ class ShearwaterRawDive { this.calculatedValues, this.headerJson, this.footerJson, + this.gf99Samples = const [], + this.startGFS, + this.gfMin, + this.gfMax, + this.decoModel, + this.startCNS, + this.endCNS, }); } @@ -115,6 +166,34 @@ LEFT JOIN log_data ld ON dd.DiveId = ld.log_id ORDER BY dd.DiveDate '''; + // The header table keys on the same id as dive_details.DiveId, and the + // sample table refers to it through diveLogId (Shearwater Cloud's own + // index, dive_log_records_diveLogId). Both tables are optional. + static const _diveLogsTable = 'dive_logs'; + static const _diveLogRecordsTable = 'dive_log_records'; + static const _diveLogColumns = [ + 'startGFS', + 'gfMin', + 'gfMax', + 'decoModel', + 'startCNS', + 'endCNS', + ]; + static const _gf99RecordColumns = ['diveLogId', 'currentTime', 'gf99']; + static const _gf99Query = ''' +SELECT currentTime, gf99 FROM dive_log_records +WHERE diveLogId = ? AND currentTime IS NOT NULL AND gf99 IS NOT NULL +ORDER BY currentTime +'''; + + /// Shearwater's deco model codes, as `dive_logs.decoModel` stores them. + static const _decoModelNames = { + 0: 'GF', + 1: 'VPM-B', + 2: 'VPM-B/GFS', + 3: 'DCIEM', + }; + /// Synchronous companion to [isShearwaterCloudDb] for callers that /// have already probed the SQLite table set (e.g. the format detector /// runs every DB-flavor check against one probe to avoid doubling @@ -179,8 +258,26 @@ ORDER BY dd.DiveDate await tempFile.writeAsBytes(bytes); final db = sqlite3.open(tempPath, mode: OpenMode.readOnly); try { + final tables = _listTables(db); + final diveLogColumns = tables.contains(_diveLogsTable) + ? _listColumns(db, _diveLogsTable) + : const {}; + final hasGf99Records = + tables.contains(_diveLogRecordsTable) && + _gf99RecordColumns.every( + _listColumns(db, _diveLogRecordsTable).contains, + ); final rows = db.select(_query); - return rows.map(_rowToRawDive).toList(); + return rows.map((row) { + final diveId = row['DiveId'].toString(); + return _rowToRawDive( + row, + header: _readDiveLogHeader(db, diveId, diveLogColumns), + gf99Samples: hasGf99Records + ? _readGf99Samples(db, diveId) + : const [], + ); + }).toList(); } finally { db.close(); } @@ -209,7 +306,87 @@ ORDER BY dd.DiveDate return rows.map((r) => r['name'] as String).toSet(); } - static ShearwaterRawDive _rowToRawDive(Row row) { + /// Column names of [table]. PRAGMA takes no bind parameters; [table] is + /// always one of this class's constants, never user input. + static Set _listColumns(Database db, String table) { + final rows = db.select('PRAGMA table_info($table)'); + return rows.map((r) => r['name'] as String).toSet(); + } + + /// The dive's `dive_logs` row, restricted to the tissue columns the export + /// actually has. Null when the table, all of those columns, or the row is + /// missing. + static Row? _readDiveLogHeader( + Database db, + String diveId, + Set availableColumns, + ) { + final columns = _diveLogColumns.where(availableColumns.contains).toList(); + if (columns.isEmpty) return null; + final rows = db.select( + 'SELECT ${columns.join(', ')} FROM dive_logs WHERE diveId = ? LIMIT 1', + [diveId], + ); + return rows.isEmpty ? null : rows.first; + } + + /// The dive's GF99 series from `dive_log_records`, ordered by time. + static List _readGf99Samples( + Database db, + String diveId, + ) { + final rows = db.select(_gf99Query, [diveId]); + final times = []; + final values = []; + for (final row in rows) { + final time = _int(row['currentTime']); + final gf99 = _int(row['gf99']); + if (time == null || gf99 == null) continue; + times.add(time); + values.add(gf99); + } + if (times.isEmpty) return const []; + final divisor = _currentTimeDivisor(times); + return List.unmodifiable([ + for (var i = 0; i < times.length; i++) + ShearwaterGf99Sample( + timeSeconds: (times[i] / divisor).round(), + gf99: values[i], + ), + ]); + } + + /// Shearwater Cloud writes `currentTime` in milliseconds (Shearwater + /// Desktop wrote seconds). Rather than trust one or the other, read the + /// spacing of the rows: the computers log every 10 s (a few every 5 s or + /// 2 s), so a median gap of 1000 or more can only be milliseconds. + static int _currentTimeDivisor(List sortedTimes) { + final deltas = []; + for (var i = 1; i < sortedTimes.length; i++) { + final delta = sortedTimes[i] - sortedTimes[i - 1]; + if (delta > 0) deltas.add(delta); + } + if (deltas.isEmpty) return 1000; + deltas.sort(); + return deltas[deltas.length ~/ 2] >= 1000 ? 1000 : 1; + } + + /// Spells `dive_logs.decoModel`: an integer code in every export seen so + /// far, passed through when an export already stores a name. + static String? _decoModelName(dynamic value) { + if (value == null) return null; + if (value is int) return _decoModelNames[value]; + final text = value.toString().trim(); + if (text.isEmpty) return null; + final code = int.tryParse(text); + return code == null ? text : _decoModelNames[code]; + } + + static ShearwaterRawDive _rowToRawDive( + Row row, { + Row? header, + List gf99Samples = const [], + }) { return ShearwaterRawDive( diveId: row['DiveId'].toString(), diveDate: _str(row['DiveDate']), @@ -249,9 +426,22 @@ ORDER BY dd.DiveDate calculatedValues: _decodeJsonString( row['calculated_values_from_samples'], ), + gf99Samples: gf99Samples, + startGFS: _double(_headerValue(header, 'startGFS')), + gfMin: _int(_headerValue(header, 'gfMin')), + gfMax: _int(_headerValue(header, 'gfMax')), + decoModel: _decoModelName(_headerValue(header, 'decoModel')), + startCNS: _double(_headerValue(header, 'startCNS')), + endCNS: _double(_headerValue(header, 'endCNS')), ); } + /// A `dive_logs` value, or null when the row or the column is missing. + static dynamic _headerValue(Row? header, String column) { + if (header == null || !header.keys.contains(column)) return null; + return header[column]; + } + /// Normalizes a value to a non-empty String or null. static String? _str(dynamic value) { if (value == null) return null; diff --git a/lib/features/universal_import/data/services/shearwater_dive_mapper.dart b/lib/features/universal_import/data/services/shearwater_dive_mapper.dart index 475b046091..622a7a7dd3 100644 --- a/lib/features/universal_import/data/services/shearwater_dive_mapper.dart +++ b/lib/features/universal_import/data/services/shearwater_dive_mapper.dart @@ -4,6 +4,7 @@ import 'package:flutter/services.dart'; import 'package:libdivecomputer_plugin/libdivecomputer_plugin.dart' as pigeon; import 'package:submersion/core/constants/enums.dart'; +import 'package:submersion/features/dive_log/domain/entities/computer_tissue_snapshot.dart'; import 'package:submersion/features/dive_log/domain/entities/dive.dart'; import 'package:submersion/features/dive_sites/domain/entities/dive_site.dart'; import 'package:submersion/features/universal_import/data/models/import_enums.dart'; @@ -12,6 +13,7 @@ import 'package:submersion/features/universal_import/data/services/import_site_l import 'package:submersion/features/universal_import/data/services/parsed_dive_profile_mapper.dart'; import 'package:submersion/features/universal_import/data/services/shearwater_db_reader.dart'; import 'package:submersion/features/universal_import/data/services/shearwater_filename_parser.dart'; +import 'package:submersion/features/universal_import/data/services/shearwater_gf99_aligner.dart'; import 'package:submersion/features/universal_import/data/services/shearwater_value_mapper.dart'; /// Converts [ShearwaterRawDive] objects into `Map` entity maps @@ -36,6 +38,7 @@ class ShearwaterDiveMapper { final siteKey = siteKeyFor(rawDive); final entryFix = _gnssFix(rawDive.gnssEntryLocation); final exitFix = _gnssFix(rawDive.gnssExitLocation); + final computerTissue = _computerTissue(rawDive); return { 'importSource': 'shearwater_cloud', @@ -80,6 +83,11 @@ class ShearwaterDiveMapper { 'diveMode': _mapDiveMode(rawDive.apparatus), 'tanks': tanks, 'profile': const >[], + // From dive_logs; libdivecomputer's values replace these when the + // profile parses (mergeWithParsedDive). + 'gradientFactorLow': ?rawDive.gfMin, + 'gradientFactorHigh': ?rawDive.gfMax, + 'computerTissue': ?computerTissue, }; } @@ -130,7 +138,11 @@ class ShearwaterDiveMapper { product: vendorProduct.$2, data: logData, ); - return mergeWithParsedDive(baseMap, parsed); + return mergeWithParsedDive( + baseMap, + parsed, + gf99Samples: rawDive.gf99Samples, + ); } on MissingPluginException { rethrow; } on PlatformException catch (e) { @@ -367,6 +379,59 @@ class ShearwaterDiveMapper { return ShearwaterValueMapper.mbarToBar(mbar); } + /// The tissue state the computer reported, or null when the export carries + /// none of it. States without a single value are omitted. + static ComputerTissueSnapshot? _computerTissue(ShearwaterRawDive rawDive) { + final start = _tissueState( + surfaceGfPercent: rawDive.startGFS, + cnsPercent: rawDive.startCNS, + ); + final end = _tissueState( + gf99Percent: _endGf99(rawDive), + cnsPercent: rawDive.endCNS, + ); + if (rawDive.decoModel == null && start == null && end == null) return null; + return ComputerTissueSnapshot( + algorithm: rawDive.decoModel, + start: start, + end: end, + ); + } + + static ComputerTissueState? _tissueState({ + double? gf99Percent, + double? surfaceGfPercent, + double? cnsPercent, + }) { + if (gf99Percent == null && surfaceGfPercent == null && cnsPercent == null) { + return null; + } + return ComputerTissueState( + gf99Percent: gf99Percent, + surfaceGfPercent: surfaceGfPercent, + cnsPercent: cnsPercent, + ); + } + + /// GF99 at the end of the dive. Shearwater Cloud writes it in three + /// places; the one derived from the samples + /// (`calculated_values_from_samples.EndGF99`) is the most reliable, and + /// `dive_details.EndGF99` is a 0.0 placeholder in exports that never + /// computed it, so zero is read as absent throughout. + static double? _endGf99(ShearwaterRawDive rawDive) { + final candidates = [ + _toDouble(rawDive.calculatedValues?['EndGF99']), + rawDive.endGF99, + rawDive.gf99Samples.isEmpty + ? null + : rawDive.gf99Samples.last.gf99.toDouble(), + ]; + for (final value in candidates) { + if (value != null && value > 0) return value; + } + return null; + } + /// Parses a GNSS location string "lat,lon" into a coordinate pair. static (double, double)? _parseGnssLocation(String? gnss) { if (gnss == null || gnss.isEmpty) return null; @@ -390,11 +455,15 @@ class ShearwaterDiveMapper { } /// Merges parsed profile and deco data into the base metadata map. + /// + /// [gf99Samples] (from `dive_log_records`) are aligned onto the parsed + /// samples by nearest time and set as each sample's `'gf99'`. @visibleForTesting static Map mergeWithParsedDive( Map baseMap, - pigeon.ParsedDive parsed, - ) { + pigeon.ParsedDive parsed, { + List gf99Samples = const [], + }) { final merged = Map.from(baseMap); // Override depth/duration with parsed values (more accurate) @@ -422,8 +491,12 @@ class ShearwaterDiveMapper { }; } - // Build profile samples with all available sensor data. - merged['profile'] = ParsedDiveProfileMapper.samples(parsed); + // Build profile samples with all available sensor data, then add the + // GF99 the export logs alongside (libdivecomputer does not report it). + merged['profile'] = ShearwaterGf99Aligner.apply( + ParsedDiveProfileMapper.samples(parsed), + gf99Samples, + ); // Extract water temperature from profile samples if not already set if (merged['waterTemp'] == null) { diff --git a/lib/features/universal_import/data/services/shearwater_gf99_aligner.dart b/lib/features/universal_import/data/services/shearwater_gf99_aligner.dart new file mode 100644 index 0000000000..c961f47560 --- /dev/null +++ b/lib/features/universal_import/data/services/shearwater_gf99_aligner.dart @@ -0,0 +1,92 @@ +import 'package:submersion/features/universal_import/data/services/shearwater_db_reader.dart'; + +/// Aligns the GF99 series a Shearwater Cloud export stores in +/// `dive_log_records` onto the profile samples libdivecomputer decoded from +/// the same dive's raw bytes. +/// +/// The two series come from different sources of the same log, so their +/// timestamps normally coincide but are not guaranteed to. Each sample takes +/// the GF99 point nearest in time, provided it lies within half the sample +/// interval; a sample with no point that close is left without `gf99`. +class ShearwaterGf99Aligner { + const ShearwaterGf99Aligner._(); + + /// Returns new sample maps, each carrying `'gf99'` (whole percent, `int`) + /// when a series point lies close enough. The input maps are not mutated. + static List> apply( + List> samples, + List series, + ) { + if (samples.isEmpty || series.isEmpty) return samples; + + final sorted = List.of(series) + ..sort((a, b) => a.timeSeconds.compareTo(b.timeSeconds)); + final tolerance = _tolerance(samples, sorted); + + return samples.map((sample) { + final time = sample['timestamp']; + if (time is! num) return sample; + final nearest = _nearest(sorted, time); + if (nearest == null) return sample; + if ((nearest.timeSeconds - time).abs() > tolerance) return sample; + return {...sample, 'gf99': nearest.gf99}; + }).toList(); + } + + /// Half the typical spacing between profile samples; falls back to the + /// series spacing when the profile has a single sample, and to an exact + /// match when neither has two points. + static double _tolerance( + List> samples, + List sorted, + ) { + final sampleTimes = + samples + .map((s) => s['timestamp']) + .whereType() + .map((t) => t.toDouble()) + .toList() + ..sort(); + final interval = + _medianInterval(sampleTimes) ?? + _medianInterval(sorted.map((p) => p.timeSeconds.toDouble()).toList()); + return interval == null ? 0 : interval / 2; + } + + static double? _medianInterval(List sortedTimes) { + final deltas = []; + for (var i = 1; i < sortedTimes.length; i++) { + final delta = sortedTimes[i] - sortedTimes[i - 1]; + if (delta > 0) deltas.add(delta); + } + if (deltas.isEmpty) return null; + deltas.sort(); + return deltas[deltas.length ~/ 2]; + } + + /// Binary search for the series point nearest to [time]. + static ShearwaterGf99Sample? _nearest( + List sorted, + num time, + ) { + if (sorted.isEmpty) return null; + var low = 0; + var high = sorted.length - 1; + while (low < high) { + final mid = (low + high) ~/ 2; + if (sorted[mid].timeSeconds < time) { + low = mid + 1; + } else { + high = mid; + } + } + // `low` is the first point at or after `time`; the point before it is + // the other candidate. + final after = sorted[low]; + if (low == 0) return after; + final before = sorted[low - 1]; + final beforeDistance = (time - before.timeSeconds).abs(); + final afterDistance = (after.timeSeconds - time).abs(); + return afterDistance < beforeDistance ? after : before; + } +} diff --git a/lib/l10n/arb/app_ar.arb b/lib/l10n/arb/app_ar.arb index f18665ab74..1b5d4a44b3 100644 --- a/lib/l10n/arb/app_ar.arb +++ b/lib/l10n/arb/app_ar.arb @@ -12504,5 +12504,22 @@ "diveCenters_rental_leadAdjustmentLabel": "الأثقال الإضافية المطلوبة ({unit})", "diveCenters_rental_volumeLabel": "السعة الفعلية ({unit})", "diveCenters_rental_noteLabel": "ملاحظة", - "diveCenters_rental_deleteConfirm": "حذف ملاحظة التأجير هذه؟" + "diveCenters_rental_deleteConfirm": "حذف ملاحظة التأجير هذه؟", + "diveLog_computerTissue_title": "كمبيوتر الغوص", + "diveLog_computerTissue_algorithm": "الخوارزمية", + "diveLog_computerTissue_compartments": "{count} حجيرات", + "diveLog_computerTissue_compartmentsWithAlgorithm": "{count} حجيرات {algorithm}", + "diveLog_computerTissue_compartment": "C{number}", + "diveLog_computerTissue_endLoading": "التحميل في نهاية الغطسة", + "diveLog_computerTissue_endTensions": "توترات الأنسجة في نهاية الغطسة", + "diveLog_computerTissue_columnStart": "البداية", + "diveLog_computerTissue_columnEnd": "النهاية", + "diveLog_computerTissue_rowN2Load": "حمل N₂", + "diveLog_computerTissue_rowCns": "CNS", + "diveLog_computerTissue_rowOtu": "OTU", + "diveLog_computerTissue_rgbmNitrogen": "معامل N₂ لـ RGBM", + "diveLog_computerTissue_rgbmHelium": "معامل He لـ RGBM", + "diveLog_computerTissue_calculated": "المحسوب {value}", + "diveLog_computerTissue_labelN2": "N₂", + "diveLog_computerTissue_labelHe": "He" } diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index 6d564b1d2b..ffde4f419a 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -12604,5 +12604,22 @@ "diveCenters_rental_leadAdjustmentLabel": "Zusätzlich benötigtes Blei ({unit})", "diveCenters_rental_volumeLabel": "Tatsächliches Volumen ({unit})", "diveCenters_rental_noteLabel": "Notiz", - "diveCenters_rental_deleteConfirm": "Diese Leihnotiz löschen?" + "diveCenters_rental_deleteConfirm": "Diese Leihnotiz löschen?", + "diveLog_computerTissue_title": "Tauchcomputer", + "diveLog_computerTissue_algorithm": "Algorithmus", + "diveLog_computerTissue_compartments": "{count} Kompartimente", + "diveLog_computerTissue_compartmentsWithAlgorithm": "{count} {algorithm}-Kompartimente", + "diveLog_computerTissue_compartment": "C{number}", + "diveLog_computerTissue_endLoading": "Sättigung am Ende des Tauchgangs", + "diveLog_computerTissue_endTensions": "Gewebespannungen am Ende des Tauchgangs", + "diveLog_computerTissue_columnStart": "Start", + "diveLog_computerTissue_columnEnd": "Ende", + "diveLog_computerTissue_rowN2Load": "N₂-Sättigung", + "diveLog_computerTissue_rowCns": "CNS", + "diveLog_computerTissue_rowOtu": "OTU", + "diveLog_computerTissue_rgbmNitrogen": "RGBM-N₂-Faktor", + "diveLog_computerTissue_rgbmHelium": "RGBM-He-Faktor", + "diveLog_computerTissue_calculated": "berechnet {value}", + "diveLog_computerTissue_labelN2": "N₂", + "diveLog_computerTissue_labelHe": "He" } diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 65c1282876..f8eb91a717 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -4472,6 +4472,23 @@ "diveLog_tank_regulatorLabel": "Regulator", "diveLog_tank_regulatorNone": "None", "diveLog_tissue_title": "Tissue Loading", + "diveLog_computerTissue_title": "Dive computer", + "diveLog_computerTissue_algorithm": "Algorithm", + "diveLog_computerTissue_compartments": "{count} compartments", + "diveLog_computerTissue_compartmentsWithAlgorithm": "{count} {algorithm} compartments", + "diveLog_computerTissue_compartment": "C{number}", + "diveLog_computerTissue_endLoading": "Loading at end of dive", + "diveLog_computerTissue_endTensions": "Tissue tensions at end of dive", + "diveLog_computerTissue_columnStart": "Start", + "diveLog_computerTissue_columnEnd": "End", + "diveLog_computerTissue_rowN2Load": "N₂ load", + "diveLog_computerTissue_rowCns": "CNS", + "diveLog_computerTissue_rowOtu": "OTU", + "diveLog_computerTissue_rgbmNitrogen": "RGBM N₂ factor", + "diveLog_computerTissue_rgbmHelium": "RGBM He factor", + "diveLog_computerTissue_calculated": "calculated {value}", + "diveLog_computerTissue_labelN2": "N₂", + "diveLog_computerTissue_labelHe": "He", "diveLog_tooltip_avgCalculated": "(avg, calculated)", "diveLog_tooltip_ceiling": "Ceiling", "diveLog_tooltip_decoStop": "Deco stop", @@ -4744,6 +4761,41 @@ } } }, + "@diveLog_computerTissue_compartments": { + "description": "Compartment count label under the dive computer tissue list when the algorithm name is unknown", + "placeholders": { + "count": { + "type": "Object" + } + } + }, + "@diveLog_computerTissue_compartmentsWithAlgorithm": { + "description": "Compartment count label under the dive computer tissue list, e.g. 15 RGBM compartments", + "placeholders": { + "count": { + "type": "Object" + }, + "algorithm": { + "type": "Object" + } + } + }, + "@diveLog_computerTissue_compartment": { + "description": "Short per-compartment label, e.g. C1", + "placeholders": { + "number": { + "type": "Object" + } + } + }, + "@diveLog_computerTissue_calculated": { + "description": "Secondary line under a dive-computer tissue value giving the app's own calculated value", + "placeholders": { + "value": { + "type": "Object" + } + } + }, "@diveLog_detail_buddyCount": { "placeholders": { "count": { diff --git a/lib/l10n/arb/app_es.arb b/lib/l10n/arb/app_es.arb index 9e45bc940c..e83a76c68d 100644 --- a/lib/l10n/arb/app_es.arb +++ b/lib/l10n/arb/app_es.arb @@ -12504,5 +12504,22 @@ "diveCenters_rental_leadAdjustmentLabel": "Lastre adicional necesario ({unit})", "diveCenters_rental_volumeLabel": "Capacidad real ({unit})", "diveCenters_rental_noteLabel": "Nota", - "diveCenters_rental_deleteConfirm": "¿Eliminar esta nota de alquiler?" + "diveCenters_rental_deleteConfirm": "¿Eliminar esta nota de alquiler?", + "diveLog_computerTissue_title": "Ordenador de buceo", + "diveLog_computerTissue_algorithm": "Algoritmo", + "diveLog_computerTissue_compartments": "{count} compartimentos", + "diveLog_computerTissue_compartmentsWithAlgorithm": "{count} compartimentos {algorithm}", + "diveLog_computerTissue_compartment": "C{number}", + "diveLog_computerTissue_endLoading": "Carga al final de la inmersión", + "diveLog_computerTissue_endTensions": "Tensiones tisulares al final de la inmersión", + "diveLog_computerTissue_columnStart": "Inicio", + "diveLog_computerTissue_columnEnd": "Fin", + "diveLog_computerTissue_rowN2Load": "Carga de N₂", + "diveLog_computerTissue_rowCns": "CNS", + "diveLog_computerTissue_rowOtu": "OTU", + "diveLog_computerTissue_rgbmNitrogen": "Factor N₂ RGBM", + "diveLog_computerTissue_rgbmHelium": "Factor He RGBM", + "diveLog_computerTissue_calculated": "calculado {value}", + "diveLog_computerTissue_labelN2": "N₂", + "diveLog_computerTissue_labelHe": "He" } diff --git a/lib/l10n/arb/app_fr.arb b/lib/l10n/arb/app_fr.arb index b5a95c96e1..7011ded8b2 100644 --- a/lib/l10n/arb/app_fr.arb +++ b/lib/l10n/arb/app_fr.arb @@ -12504,5 +12504,22 @@ "diveCenters_rental_leadAdjustmentLabel": "Lest supplémentaire nécessaire ({unit})", "diveCenters_rental_volumeLabel": "Capacité réelle ({unit})", "diveCenters_rental_noteLabel": "Note", - "diveCenters_rental_deleteConfirm": "Supprimer cette note de location ?" + "diveCenters_rental_deleteConfirm": "Supprimer cette note de location ?", + "diveLog_computerTissue_title": "Ordinateur de plongée", + "diveLog_computerTissue_algorithm": "Algorithme", + "diveLog_computerTissue_compartments": "{count} compartiments", + "diveLog_computerTissue_compartmentsWithAlgorithm": "{count} compartiments {algorithm}", + "diveLog_computerTissue_compartment": "C{number}", + "diveLog_computerTissue_endLoading": "Saturation en fin de plongée", + "diveLog_computerTissue_endTensions": "Tensions tissulaires en fin de plongée", + "diveLog_computerTissue_columnStart": "Début", + "diveLog_computerTissue_columnEnd": "Fin", + "diveLog_computerTissue_rowN2Load": "Charge N₂", + "diveLog_computerTissue_rowCns": "CNS", + "diveLog_computerTissue_rowOtu": "OTU", + "diveLog_computerTissue_rgbmNitrogen": "Facteur N₂ RGBM", + "diveLog_computerTissue_rgbmHelium": "Facteur He RGBM", + "diveLog_computerTissue_calculated": "calculé {value}", + "diveLog_computerTissue_labelN2": "N₂", + "diveLog_computerTissue_labelHe": "He" } diff --git a/lib/l10n/arb/app_he.arb b/lib/l10n/arb/app_he.arb index c0b7908e3c..8021a7dba8 100644 --- a/lib/l10n/arb/app_he.arb +++ b/lib/l10n/arb/app_he.arb @@ -12504,5 +12504,22 @@ "diveCenters_rental_leadAdjustmentLabel": "משקולות נוספות נדרשות ({unit})", "diveCenters_rental_volumeLabel": "קיבולת בפועל ({unit})", "diveCenters_rental_noteLabel": "הערה", - "diveCenters_rental_deleteConfirm": "למחוק את הערת ההשכרה הזו?" + "diveCenters_rental_deleteConfirm": "למחוק את הערת ההשכרה הזו?", + "diveLog_computerTissue_title": "מחשב צלילה", + "diveLog_computerTissue_algorithm": "אלגוריתם", + "diveLog_computerTissue_compartments": "{count} מדורים", + "diveLog_computerTissue_compartmentsWithAlgorithm": "{count} מדורי {algorithm}", + "diveLog_computerTissue_compartment": "C{number}", + "diveLog_computerTissue_endLoading": "טעינה בסוף הצלילה", + "diveLog_computerTissue_endTensions": "מתחי רקמות בסוף הצלילה", + "diveLog_computerTissue_columnStart": "התחלה", + "diveLog_computerTissue_columnEnd": "סיום", + "diveLog_computerTissue_rowN2Load": "עומס N₂", + "diveLog_computerTissue_rowCns": "CNS", + "diveLog_computerTissue_rowOtu": "OTU", + "diveLog_computerTissue_rgbmNitrogen": "מקדם N₂ של RGBM", + "diveLog_computerTissue_rgbmHelium": "מקדם He של RGBM", + "diveLog_computerTissue_calculated": "מחושב {value}", + "diveLog_computerTissue_labelN2": "N₂", + "diveLog_computerTissue_labelHe": "He" } diff --git a/lib/l10n/arb/app_hu.arb b/lib/l10n/arb/app_hu.arb index 68df187159..e2a0b7401b 100644 --- a/lib/l10n/arb/app_hu.arb +++ b/lib/l10n/arb/app_hu.arb @@ -12504,5 +12504,22 @@ "diveCenters_rental_leadAdjustmentLabel": "Szükséges plusz ólom ({unit})", "diveCenters_rental_volumeLabel": "Tényleges térfogat ({unit})", "diveCenters_rental_noteLabel": "Jegyzet", - "diveCenters_rental_deleteConfirm": "Törlöd ezt a bérlési jegyzetet?" + "diveCenters_rental_deleteConfirm": "Törlöd ezt a bérlési jegyzetet?", + "diveLog_computerTissue_title": "Búvárkomputer", + "diveLog_computerTissue_algorithm": "Algoritmus", + "diveLog_computerTissue_compartments": "{count} rekesz", + "diveLog_computerTissue_compartmentsWithAlgorithm": "{count} {algorithm} rekesz", + "diveLog_computerTissue_compartment": "C{number}", + "diveLog_computerTissue_endLoading": "Telítettség a merülés végén", + "diveLog_computerTissue_endTensions": "Szöveti feszültségek a merülés végén", + "diveLog_computerTissue_columnStart": "Kezdet", + "diveLog_computerTissue_columnEnd": "Vég", + "diveLog_computerTissue_rowN2Load": "N₂-terhelés", + "diveLog_computerTissue_rowCns": "CNS", + "diveLog_computerTissue_rowOtu": "OTU", + "diveLog_computerTissue_rgbmNitrogen": "RGBM N₂-tényező", + "diveLog_computerTissue_rgbmHelium": "RGBM He-tényező", + "diveLog_computerTissue_calculated": "számított {value}", + "diveLog_computerTissue_labelN2": "N₂", + "diveLog_computerTissue_labelHe": "He" } diff --git a/lib/l10n/arb/app_it.arb b/lib/l10n/arb/app_it.arb index 260e08e62f..5c9afe4231 100644 --- a/lib/l10n/arb/app_it.arb +++ b/lib/l10n/arb/app_it.arb @@ -12504,5 +12504,22 @@ "diveCenters_rental_leadAdjustmentLabel": "Zavorra aggiuntiva necessaria ({unit})", "diveCenters_rental_volumeLabel": "Capacità reale ({unit})", "diveCenters_rental_noteLabel": "Nota", - "diveCenters_rental_deleteConfirm": "Eliminare questa nota sul noleggio?" + "diveCenters_rental_deleteConfirm": "Eliminare questa nota sul noleggio?", + "diveLog_computerTissue_title": "Computer subacqueo", + "diveLog_computerTissue_algorithm": "Algoritmo", + "diveLog_computerTissue_compartments": "{count} compartimenti", + "diveLog_computerTissue_compartmentsWithAlgorithm": "{count} compartimenti {algorithm}", + "diveLog_computerTissue_compartment": "C{number}", + "diveLog_computerTissue_endLoading": "Saturazione a fine immersione", + "diveLog_computerTissue_endTensions": "Tensioni tissutali a fine immersione", + "diveLog_computerTissue_columnStart": "Inizio", + "diveLog_computerTissue_columnEnd": "Fine", + "diveLog_computerTissue_rowN2Load": "Carico N₂", + "diveLog_computerTissue_rowCns": "CNS", + "diveLog_computerTissue_rowOtu": "OTU", + "diveLog_computerTissue_rgbmNitrogen": "Fattore N₂ RGBM", + "diveLog_computerTissue_rgbmHelium": "Fattore He RGBM", + "diveLog_computerTissue_calculated": "calcolato {value}", + "diveLog_computerTissue_labelN2": "N₂", + "diveLog_computerTissue_labelHe": "He" } diff --git a/lib/l10n/arb/app_localizations.dart b/lib/l10n/arb/app_localizations.dart index 9b53da3c1e..c7a65710b5 100644 --- a/lib/l10n/arb/app_localizations.dart +++ b/lib/l10n/arb/app_localizations.dart @@ -13719,6 +13719,111 @@ abstract class AppLocalizations { /// **'Tissue Loading'** String get diveLog_tissue_title; + /// No description provided for @diveLog_computerTissue_title. + /// + /// In en, this message translates to: + /// **'Dive computer'** + String get diveLog_computerTissue_title; + + /// No description provided for @diveLog_computerTissue_algorithm. + /// + /// In en, this message translates to: + /// **'Algorithm'** + String get diveLog_computerTissue_algorithm; + + /// Compartment count label under the dive computer tissue list when the algorithm name is unknown + /// + /// In en, this message translates to: + /// **'{count} compartments'** + String diveLog_computerTissue_compartments(Object count); + + /// Compartment count label under the dive computer tissue list, e.g. 15 RGBM compartments + /// + /// In en, this message translates to: + /// **'{count} {algorithm} compartments'** + String diveLog_computerTissue_compartmentsWithAlgorithm( + Object count, + Object algorithm, + ); + + /// Short per-compartment label, e.g. C1 + /// + /// In en, this message translates to: + /// **'C{number}'** + String diveLog_computerTissue_compartment(Object number); + + /// No description provided for @diveLog_computerTissue_endLoading. + /// + /// In en, this message translates to: + /// **'Loading at end of dive'** + String get diveLog_computerTissue_endLoading; + + /// No description provided for @diveLog_computerTissue_endTensions. + /// + /// In en, this message translates to: + /// **'Tissue tensions at end of dive'** + String get diveLog_computerTissue_endTensions; + + /// No description provided for @diveLog_computerTissue_columnStart. + /// + /// In en, this message translates to: + /// **'Start'** + String get diveLog_computerTissue_columnStart; + + /// No description provided for @diveLog_computerTissue_columnEnd. + /// + /// In en, this message translates to: + /// **'End'** + String get diveLog_computerTissue_columnEnd; + + /// No description provided for @diveLog_computerTissue_rowN2Load. + /// + /// In en, this message translates to: + /// **'N₂ load'** + String get diveLog_computerTissue_rowN2Load; + + /// No description provided for @diveLog_computerTissue_rowCns. + /// + /// In en, this message translates to: + /// **'CNS'** + String get diveLog_computerTissue_rowCns; + + /// No description provided for @diveLog_computerTissue_rowOtu. + /// + /// In en, this message translates to: + /// **'OTU'** + String get diveLog_computerTissue_rowOtu; + + /// No description provided for @diveLog_computerTissue_rgbmNitrogen. + /// + /// In en, this message translates to: + /// **'RGBM N₂ factor'** + String get diveLog_computerTissue_rgbmNitrogen; + + /// No description provided for @diveLog_computerTissue_rgbmHelium. + /// + /// In en, this message translates to: + /// **'RGBM He factor'** + String get diveLog_computerTissue_rgbmHelium; + + /// Secondary line under a dive-computer tissue value giving the app's own calculated value + /// + /// In en, this message translates to: + /// **'calculated {value}'** + String diveLog_computerTissue_calculated(Object value); + + /// No description provided for @diveLog_computerTissue_labelN2. + /// + /// In en, this message translates to: + /// **'N₂'** + String get diveLog_computerTissue_labelN2; + + /// No description provided for @diveLog_computerTissue_labelHe. + /// + /// In en, this message translates to: + /// **'He'** + String get diveLog_computerTissue_labelHe; + /// No description provided for @diveLog_tooltip_avgCalculated. /// /// 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 a99fbb8735..e450ff51aa 100644 --- a/lib/l10n/arb/app_localizations_ar.dart +++ b/lib/l10n/arb/app_localizations_ar.dart @@ -8233,6 +8233,69 @@ class AppLocalizationsAr extends AppLocalizations { @override String get diveLog_tissue_title => 'تحميل الأنسجة'; + @override + String get diveLog_computerTissue_title => 'كمبيوتر الغوص'; + + @override + String get diveLog_computerTissue_algorithm => 'الخوارزمية'; + + @override + String diveLog_computerTissue_compartments(Object count) { + return '$count حجيرات'; + } + + @override + String diveLog_computerTissue_compartmentsWithAlgorithm( + Object count, + Object algorithm, + ) { + return '$count حجيرات $algorithm'; + } + + @override + String diveLog_computerTissue_compartment(Object number) { + return 'C$number'; + } + + @override + String get diveLog_computerTissue_endLoading => 'التحميل في نهاية الغطسة'; + + @override + String get diveLog_computerTissue_endTensions => + 'توترات الأنسجة في نهاية الغطسة'; + + @override + String get diveLog_computerTissue_columnStart => 'البداية'; + + @override + String get diveLog_computerTissue_columnEnd => 'النهاية'; + + @override + String get diveLog_computerTissue_rowN2Load => 'حمل N₂'; + + @override + String get diveLog_computerTissue_rowCns => 'CNS'; + + @override + String get diveLog_computerTissue_rowOtu => 'OTU'; + + @override + String get diveLog_computerTissue_rgbmNitrogen => 'معامل N₂ لـ RGBM'; + + @override + String get diveLog_computerTissue_rgbmHelium => 'معامل He لـ RGBM'; + + @override + String diveLog_computerTissue_calculated(Object value) { + return 'المحسوب $value'; + } + + @override + String get diveLog_computerTissue_labelN2 => 'N₂'; + + @override + String get diveLog_computerTissue_labelHe => 'He'; + @override String get diveLog_tooltip_avgCalculated => '(متوسط، محسوب)'; diff --git a/lib/l10n/arb/app_localizations_de.dart b/lib/l10n/arb/app_localizations_de.dart index eff1a05a80..a1ac7eca50 100644 --- a/lib/l10n/arb/app_localizations_de.dart +++ b/lib/l10n/arb/app_localizations_de.dart @@ -8389,6 +8389,70 @@ class AppLocalizationsDe extends AppLocalizations { @override String get diveLog_tissue_title => 'Gewebesättigung'; + @override + String get diveLog_computerTissue_title => 'Tauchcomputer'; + + @override + String get diveLog_computerTissue_algorithm => 'Algorithmus'; + + @override + String diveLog_computerTissue_compartments(Object count) { + return '$count Kompartimente'; + } + + @override + String diveLog_computerTissue_compartmentsWithAlgorithm( + Object count, + Object algorithm, + ) { + return '$count $algorithm-Kompartimente'; + } + + @override + String diveLog_computerTissue_compartment(Object number) { + return 'C$number'; + } + + @override + String get diveLog_computerTissue_endLoading => + 'Sättigung am Ende des Tauchgangs'; + + @override + String get diveLog_computerTissue_endTensions => + 'Gewebespannungen am Ende des Tauchgangs'; + + @override + String get diveLog_computerTissue_columnStart => 'Start'; + + @override + String get diveLog_computerTissue_columnEnd => 'Ende'; + + @override + String get diveLog_computerTissue_rowN2Load => 'N₂-Sättigung'; + + @override + String get diveLog_computerTissue_rowCns => 'CNS'; + + @override + String get diveLog_computerTissue_rowOtu => 'OTU'; + + @override + String get diveLog_computerTissue_rgbmNitrogen => 'RGBM-N₂-Faktor'; + + @override + String get diveLog_computerTissue_rgbmHelium => 'RGBM-He-Faktor'; + + @override + String diveLog_computerTissue_calculated(Object value) { + return 'berechnet $value'; + } + + @override + String get diveLog_computerTissue_labelN2 => 'N₂'; + + @override + String get diveLog_computerTissue_labelHe => 'He'; + @override String get diveLog_tooltip_avgCalculated => '(Durchschn., berechnet)'; diff --git a/lib/l10n/arb/app_localizations_en.dart b/lib/l10n/arb/app_localizations_en.dart index 6c071576da..d06ea8c33c 100644 --- a/lib/l10n/arb/app_localizations_en.dart +++ b/lib/l10n/arb/app_localizations_en.dart @@ -8248,6 +8248,69 @@ class AppLocalizationsEn extends AppLocalizations { @override String get diveLog_tissue_title => 'Tissue Loading'; + @override + String get diveLog_computerTissue_title => 'Dive computer'; + + @override + String get diveLog_computerTissue_algorithm => 'Algorithm'; + + @override + String diveLog_computerTissue_compartments(Object count) { + return '$count compartments'; + } + + @override + String diveLog_computerTissue_compartmentsWithAlgorithm( + Object count, + Object algorithm, + ) { + return '$count $algorithm compartments'; + } + + @override + String diveLog_computerTissue_compartment(Object number) { + return 'C$number'; + } + + @override + String get diveLog_computerTissue_endLoading => 'Loading at end of dive'; + + @override + String get diveLog_computerTissue_endTensions => + 'Tissue tensions at end of dive'; + + @override + String get diveLog_computerTissue_columnStart => 'Start'; + + @override + String get diveLog_computerTissue_columnEnd => 'End'; + + @override + String get diveLog_computerTissue_rowN2Load => 'N₂ load'; + + @override + String get diveLog_computerTissue_rowCns => 'CNS'; + + @override + String get diveLog_computerTissue_rowOtu => 'OTU'; + + @override + String get diveLog_computerTissue_rgbmNitrogen => 'RGBM N₂ factor'; + + @override + String get diveLog_computerTissue_rgbmHelium => 'RGBM He factor'; + + @override + String diveLog_computerTissue_calculated(Object value) { + return 'calculated $value'; + } + + @override + String get diveLog_computerTissue_labelN2 => 'N₂'; + + @override + String get diveLog_computerTissue_labelHe => 'He'; + @override String get diveLog_tooltip_avgCalculated => '(avg, calculated)'; diff --git a/lib/l10n/arb/app_localizations_es.dart b/lib/l10n/arb/app_localizations_es.dart index 619e818072..0fab1ac45a 100644 --- a/lib/l10n/arb/app_localizations_es.dart +++ b/lib/l10n/arb/app_localizations_es.dart @@ -8395,6 +8395,70 @@ class AppLocalizationsEs extends AppLocalizations { @override String get diveLog_tissue_title => 'Carga tisular'; + @override + String get diveLog_computerTissue_title => 'Ordenador de buceo'; + + @override + String get diveLog_computerTissue_algorithm => 'Algoritmo'; + + @override + String diveLog_computerTissue_compartments(Object count) { + return '$count compartimentos'; + } + + @override + String diveLog_computerTissue_compartmentsWithAlgorithm( + Object count, + Object algorithm, + ) { + return '$count compartimentos $algorithm'; + } + + @override + String diveLog_computerTissue_compartment(Object number) { + return 'C$number'; + } + + @override + String get diveLog_computerTissue_endLoading => + 'Carga al final de la inmersión'; + + @override + String get diveLog_computerTissue_endTensions => + 'Tensiones tisulares al final de la inmersión'; + + @override + String get diveLog_computerTissue_columnStart => 'Inicio'; + + @override + String get diveLog_computerTissue_columnEnd => 'Fin'; + + @override + String get diveLog_computerTissue_rowN2Load => 'Carga de N₂'; + + @override + String get diveLog_computerTissue_rowCns => 'CNS'; + + @override + String get diveLog_computerTissue_rowOtu => 'OTU'; + + @override + String get diveLog_computerTissue_rgbmNitrogen => 'Factor N₂ RGBM'; + + @override + String get diveLog_computerTissue_rgbmHelium => 'Factor He RGBM'; + + @override + String diveLog_computerTissue_calculated(Object value) { + return 'calculado $value'; + } + + @override + String get diveLog_computerTissue_labelN2 => 'N₂'; + + @override + String get diveLog_computerTissue_labelHe => 'He'; + @override String get diveLog_tooltip_avgCalculated => '(prom., calculado)'; diff --git a/lib/l10n/arb/app_localizations_fr.dart b/lib/l10n/arb/app_localizations_fr.dart index 6594148db0..b88ae1768c 100644 --- a/lib/l10n/arb/app_localizations_fr.dart +++ b/lib/l10n/arb/app_localizations_fr.dart @@ -8434,6 +8434,70 @@ class AppLocalizationsFr extends AppLocalizations { @override String get diveLog_tissue_title => 'Charge tissulaire'; + @override + String get diveLog_computerTissue_title => 'Ordinateur de plongée'; + + @override + String get diveLog_computerTissue_algorithm => 'Algorithme'; + + @override + String diveLog_computerTissue_compartments(Object count) { + return '$count compartiments'; + } + + @override + String diveLog_computerTissue_compartmentsWithAlgorithm( + Object count, + Object algorithm, + ) { + return '$count compartiments $algorithm'; + } + + @override + String diveLog_computerTissue_compartment(Object number) { + return 'C$number'; + } + + @override + String get diveLog_computerTissue_endLoading => + 'Saturation en fin de plongée'; + + @override + String get diveLog_computerTissue_endTensions => + 'Tensions tissulaires en fin de plongée'; + + @override + String get diveLog_computerTissue_columnStart => 'Début'; + + @override + String get diveLog_computerTissue_columnEnd => 'Fin'; + + @override + String get diveLog_computerTissue_rowN2Load => 'Charge N₂'; + + @override + String get diveLog_computerTissue_rowCns => 'CNS'; + + @override + String get diveLog_computerTissue_rowOtu => 'OTU'; + + @override + String get diveLog_computerTissue_rgbmNitrogen => 'Facteur N₂ RGBM'; + + @override + String get diveLog_computerTissue_rgbmHelium => 'Facteur He RGBM'; + + @override + String diveLog_computerTissue_calculated(Object value) { + return 'calculé $value'; + } + + @override + String get diveLog_computerTissue_labelN2 => 'N₂'; + + @override + String get diveLog_computerTissue_labelHe => 'He'; + @override String get diveLog_tooltip_avgCalculated => '(moy., calculé)'; diff --git a/lib/l10n/arb/app_localizations_he.dart b/lib/l10n/arb/app_localizations_he.dart index 08b04daca1..0ca45e4873 100644 --- a/lib/l10n/arb/app_localizations_he.dart +++ b/lib/l10n/arb/app_localizations_he.dart @@ -8187,6 +8187,68 @@ class AppLocalizationsHe extends AppLocalizations { @override String get diveLog_tissue_title => 'עומס רקמות'; + @override + String get diveLog_computerTissue_title => 'מחשב צלילה'; + + @override + String get diveLog_computerTissue_algorithm => 'אלגוריתם'; + + @override + String diveLog_computerTissue_compartments(Object count) { + return '$count מדורים'; + } + + @override + String diveLog_computerTissue_compartmentsWithAlgorithm( + Object count, + Object algorithm, + ) { + return '$count מדורי $algorithm'; + } + + @override + String diveLog_computerTissue_compartment(Object number) { + return 'C$number'; + } + + @override + String get diveLog_computerTissue_endLoading => 'טעינה בסוף הצלילה'; + + @override + String get diveLog_computerTissue_endTensions => 'מתחי רקמות בסוף הצלילה'; + + @override + String get diveLog_computerTissue_columnStart => 'התחלה'; + + @override + String get diveLog_computerTissue_columnEnd => 'סיום'; + + @override + String get diveLog_computerTissue_rowN2Load => 'עומס N₂'; + + @override + String get diveLog_computerTissue_rowCns => 'CNS'; + + @override + String get diveLog_computerTissue_rowOtu => 'OTU'; + + @override + String get diveLog_computerTissue_rgbmNitrogen => 'מקדם N₂ של RGBM'; + + @override + String get diveLog_computerTissue_rgbmHelium => 'מקדם He של RGBM'; + + @override + String diveLog_computerTissue_calculated(Object value) { + return 'מחושב $value'; + } + + @override + String get diveLog_computerTissue_labelN2 => 'N₂'; + + @override + String get diveLog_computerTissue_labelHe => 'He'; + @override String get diveLog_tooltip_avgCalculated => '(ממוצע, מחושב)'; diff --git a/lib/l10n/arb/app_localizations_hu.dart b/lib/l10n/arb/app_localizations_hu.dart index 01006fa755..18b84a376b 100644 --- a/lib/l10n/arb/app_localizations_hu.dart +++ b/lib/l10n/arb/app_localizations_hu.dart @@ -8377,6 +8377,69 @@ class AppLocalizationsHu extends AppLocalizations { @override String get diveLog_tissue_title => 'Szövettelítődés'; + @override + String get diveLog_computerTissue_title => 'Búvárkomputer'; + + @override + String get diveLog_computerTissue_algorithm => 'Algoritmus'; + + @override + String diveLog_computerTissue_compartments(Object count) { + return '$count rekesz'; + } + + @override + String diveLog_computerTissue_compartmentsWithAlgorithm( + Object count, + Object algorithm, + ) { + return '$count $algorithm rekesz'; + } + + @override + String diveLog_computerTissue_compartment(Object number) { + return 'C$number'; + } + + @override + String get diveLog_computerTissue_endLoading => 'Telítettség a merülés végén'; + + @override + String get diveLog_computerTissue_endTensions => + 'Szöveti feszültségek a merülés végén'; + + @override + String get diveLog_computerTissue_columnStart => 'Kezdet'; + + @override + String get diveLog_computerTissue_columnEnd => 'Vég'; + + @override + String get diveLog_computerTissue_rowN2Load => 'N₂-terhelés'; + + @override + String get diveLog_computerTissue_rowCns => 'CNS'; + + @override + String get diveLog_computerTissue_rowOtu => 'OTU'; + + @override + String get diveLog_computerTissue_rgbmNitrogen => 'RGBM N₂-tényező'; + + @override + String get diveLog_computerTissue_rgbmHelium => 'RGBM He-tényező'; + + @override + String diveLog_computerTissue_calculated(Object value) { + return 'számított $value'; + } + + @override + String get diveLog_computerTissue_labelN2 => 'N₂'; + + @override + String get diveLog_computerTissue_labelHe => 'He'; + @override String get diveLog_tooltip_avgCalculated => '(átl., számított)'; diff --git a/lib/l10n/arb/app_localizations_it.dart b/lib/l10n/arb/app_localizations_it.dart index 6418182905..f7e4e4efb9 100644 --- a/lib/l10n/arb/app_localizations_it.dart +++ b/lib/l10n/arb/app_localizations_it.dart @@ -8396,6 +8396,70 @@ class AppLocalizationsIt extends AppLocalizations { @override String get diveLog_tissue_title => 'Carico tissutale'; + @override + String get diveLog_computerTissue_title => 'Computer subacqueo'; + + @override + String get diveLog_computerTissue_algorithm => 'Algoritmo'; + + @override + String diveLog_computerTissue_compartments(Object count) { + return '$count compartimenti'; + } + + @override + String diveLog_computerTissue_compartmentsWithAlgorithm( + Object count, + Object algorithm, + ) { + return '$count compartimenti $algorithm'; + } + + @override + String diveLog_computerTissue_compartment(Object number) { + return 'C$number'; + } + + @override + String get diveLog_computerTissue_endLoading => + 'Saturazione a fine immersione'; + + @override + String get diveLog_computerTissue_endTensions => + 'Tensioni tissutali a fine immersione'; + + @override + String get diveLog_computerTissue_columnStart => 'Inizio'; + + @override + String get diveLog_computerTissue_columnEnd => 'Fine'; + + @override + String get diveLog_computerTissue_rowN2Load => 'Carico N₂'; + + @override + String get diveLog_computerTissue_rowCns => 'CNS'; + + @override + String get diveLog_computerTissue_rowOtu => 'OTU'; + + @override + String get diveLog_computerTissue_rgbmNitrogen => 'Fattore N₂ RGBM'; + + @override + String get diveLog_computerTissue_rgbmHelium => 'Fattore He RGBM'; + + @override + String diveLog_computerTissue_calculated(Object value) { + return 'calcolato $value'; + } + + @override + String get diveLog_computerTissue_labelN2 => 'N₂'; + + @override + String get diveLog_computerTissue_labelHe => 'He'; + @override String get diveLog_tooltip_avgCalculated => '(media, calcolato)'; diff --git a/lib/l10n/arb/app_localizations_nl.dart b/lib/l10n/arb/app_localizations_nl.dart index a9ed7bce4d..fc394a5e18 100644 --- a/lib/l10n/arb/app_localizations_nl.dart +++ b/lib/l10n/arb/app_localizations_nl.dart @@ -8331,6 +8331,70 @@ class AppLocalizationsNl extends AppLocalizations { @override String get diveLog_tissue_title => 'Weefselbelasting'; + @override + String get diveLog_computerTissue_title => 'Duikcomputer'; + + @override + String get diveLog_computerTissue_algorithm => 'Algoritme'; + + @override + String diveLog_computerTissue_compartments(Object count) { + return '$count compartimenten'; + } + + @override + String diveLog_computerTissue_compartmentsWithAlgorithm( + Object count, + Object algorithm, + ) { + return '$count $algorithm-compartimenten'; + } + + @override + String diveLog_computerTissue_compartment(Object number) { + return 'C$number'; + } + + @override + String get diveLog_computerTissue_endLoading => + 'Belasting aan het einde van de duik'; + + @override + String get diveLog_computerTissue_endTensions => + 'Weefselspanningen aan het einde van de duik'; + + @override + String get diveLog_computerTissue_columnStart => 'Start'; + + @override + String get diveLog_computerTissue_columnEnd => 'Einde'; + + @override + String get diveLog_computerTissue_rowN2Load => 'N₂-belasting'; + + @override + String get diveLog_computerTissue_rowCns => 'CNS'; + + @override + String get diveLog_computerTissue_rowOtu => 'OTU'; + + @override + String get diveLog_computerTissue_rgbmNitrogen => 'RGBM N₂-factor'; + + @override + String get diveLog_computerTissue_rgbmHelium => 'RGBM He-factor'; + + @override + String diveLog_computerTissue_calculated(Object value) { + return 'berekend $value'; + } + + @override + String get diveLog_computerTissue_labelN2 => 'N₂'; + + @override + String get diveLog_computerTissue_labelHe => 'He'; + @override String get diveLog_tooltip_avgCalculated => '(gem., berekend)'; diff --git a/lib/l10n/arb/app_localizations_pt.dart b/lib/l10n/arb/app_localizations_pt.dart index 80601b7c51..2a4bfea0ee 100644 --- a/lib/l10n/arb/app_localizations_pt.dart +++ b/lib/l10n/arb/app_localizations_pt.dart @@ -8396,6 +8396,69 @@ class AppLocalizationsPt extends AppLocalizations { @override String get diveLog_tissue_title => 'Carga Tissular'; + @override + String get diveLog_computerTissue_title => 'Computador de mergulho'; + + @override + String get diveLog_computerTissue_algorithm => 'Algoritmo'; + + @override + String diveLog_computerTissue_compartments(Object count) { + return '$count compartimentos'; + } + + @override + String diveLog_computerTissue_compartmentsWithAlgorithm( + Object count, + Object algorithm, + ) { + return '$count compartimentos $algorithm'; + } + + @override + String diveLog_computerTissue_compartment(Object number) { + return 'C$number'; + } + + @override + String get diveLog_computerTissue_endLoading => 'Carga no fim do mergulho'; + + @override + String get diveLog_computerTissue_endTensions => + 'Tensões teciduais no fim do mergulho'; + + @override + String get diveLog_computerTissue_columnStart => 'Início'; + + @override + String get diveLog_computerTissue_columnEnd => 'Fim'; + + @override + String get diveLog_computerTissue_rowN2Load => 'Carga de N₂'; + + @override + String get diveLog_computerTissue_rowCns => 'CNS'; + + @override + String get diveLog_computerTissue_rowOtu => 'OTU'; + + @override + String get diveLog_computerTissue_rgbmNitrogen => 'Fator N₂ RGBM'; + + @override + String get diveLog_computerTissue_rgbmHelium => 'Fator He RGBM'; + + @override + String diveLog_computerTissue_calculated(Object value) { + return 'calculado $value'; + } + + @override + String get diveLog_computerTissue_labelN2 => 'N₂'; + + @override + String get diveLog_computerTissue_labelHe => 'He'; + @override String get diveLog_tooltip_avgCalculated => '(méd., calculado)'; diff --git a/lib/l10n/arb/app_localizations_zh.dart b/lib/l10n/arb/app_localizations_zh.dart index 5de615cbf6..72bf3b17b4 100644 --- a/lib/l10n/arb/app_localizations_zh.dart +++ b/lib/l10n/arb/app_localizations_zh.dart @@ -7978,6 +7978,68 @@ class AppLocalizationsZh extends AppLocalizations { @override String get diveLog_tissue_title => '组织饱和度'; + @override + String get diveLog_computerTissue_title => '潜水电脑'; + + @override + String get diveLog_computerTissue_algorithm => '算法'; + + @override + String diveLog_computerTissue_compartments(Object count) { + return '$count 个组织舱'; + } + + @override + String diveLog_computerTissue_compartmentsWithAlgorithm( + Object count, + Object algorithm, + ) { + return '$count 个 $algorithm 组织舱'; + } + + @override + String diveLog_computerTissue_compartment(Object number) { + return 'C$number'; + } + + @override + String get diveLog_computerTissue_endLoading => '潜水结束时的负荷'; + + @override + String get diveLog_computerTissue_endTensions => '潜水结束时的组织张力'; + + @override + String get diveLog_computerTissue_columnStart => '开始'; + + @override + String get diveLog_computerTissue_columnEnd => '结束'; + + @override + String get diveLog_computerTissue_rowN2Load => 'N₂ 负荷'; + + @override + String get diveLog_computerTissue_rowCns => 'CNS'; + + @override + String get diveLog_computerTissue_rowOtu => 'OTU'; + + @override + String get diveLog_computerTissue_rgbmNitrogen => 'RGBM N₂ 系数'; + + @override + String get diveLog_computerTissue_rgbmHelium => 'RGBM He 系数'; + + @override + String diveLog_computerTissue_calculated(Object value) { + return '计算值 $value'; + } + + @override + String get diveLog_computerTissue_labelN2 => 'N₂'; + + @override + String get diveLog_computerTissue_labelHe => 'He'; + @override String get diveLog_tooltip_avgCalculated => '(平均值,计算)'; diff --git a/lib/l10n/arb/app_nl.arb b/lib/l10n/arb/app_nl.arb index 387cd290af..510772684a 100644 --- a/lib/l10n/arb/app_nl.arb +++ b/lib/l10n/arb/app_nl.arb @@ -12504,5 +12504,22 @@ "diveCenters_rental_leadAdjustmentLabel": "Extra lood nodig ({unit})", "diveCenters_rental_volumeLabel": "Werkelijke inhoud ({unit})", "diveCenters_rental_noteLabel": "Notitie", - "diveCenters_rental_deleteConfirm": "Deze huurnotitie verwijderen?" + "diveCenters_rental_deleteConfirm": "Deze huurnotitie verwijderen?", + "diveLog_computerTissue_title": "Duikcomputer", + "diveLog_computerTissue_algorithm": "Algoritme", + "diveLog_computerTissue_compartments": "{count} compartimenten", + "diveLog_computerTissue_compartmentsWithAlgorithm": "{count} {algorithm}-compartimenten", + "diveLog_computerTissue_compartment": "C{number}", + "diveLog_computerTissue_endLoading": "Belasting aan het einde van de duik", + "diveLog_computerTissue_endTensions": "Weefselspanningen aan het einde van de duik", + "diveLog_computerTissue_columnStart": "Start", + "diveLog_computerTissue_columnEnd": "Einde", + "diveLog_computerTissue_rowN2Load": "N₂-belasting", + "diveLog_computerTissue_rowCns": "CNS", + "diveLog_computerTissue_rowOtu": "OTU", + "diveLog_computerTissue_rgbmNitrogen": "RGBM N₂-factor", + "diveLog_computerTissue_rgbmHelium": "RGBM He-factor", + "diveLog_computerTissue_calculated": "berekend {value}", + "diveLog_computerTissue_labelN2": "N₂", + "diveLog_computerTissue_labelHe": "He" } diff --git a/lib/l10n/arb/app_pt.arb b/lib/l10n/arb/app_pt.arb index 0d09c8bc08..bc31dd7018 100644 --- a/lib/l10n/arb/app_pt.arb +++ b/lib/l10n/arb/app_pt.arb @@ -12504,5 +12504,22 @@ "diveCenters_rental_leadAdjustmentLabel": "Lastro adicional necessário ({unit})", "diveCenters_rental_volumeLabel": "Capacidade real ({unit})", "diveCenters_rental_noteLabel": "Nota", - "diveCenters_rental_deleteConfirm": "Eliminar esta nota de aluguer?" + "diveCenters_rental_deleteConfirm": "Eliminar esta nota de aluguer?", + "diveLog_computerTissue_title": "Computador de mergulho", + "diveLog_computerTissue_algorithm": "Algoritmo", + "diveLog_computerTissue_compartments": "{count} compartimentos", + "diveLog_computerTissue_compartmentsWithAlgorithm": "{count} compartimentos {algorithm}", + "diveLog_computerTissue_compartment": "C{number}", + "diveLog_computerTissue_endLoading": "Carga no fim do mergulho", + "diveLog_computerTissue_endTensions": "Tensões teciduais no fim do mergulho", + "diveLog_computerTissue_columnStart": "Início", + "diveLog_computerTissue_columnEnd": "Fim", + "diveLog_computerTissue_rowN2Load": "Carga de N₂", + "diveLog_computerTissue_rowCns": "CNS", + "diveLog_computerTissue_rowOtu": "OTU", + "diveLog_computerTissue_rgbmNitrogen": "Fator N₂ RGBM", + "diveLog_computerTissue_rgbmHelium": "Fator He RGBM", + "diveLog_computerTissue_calculated": "calculado {value}", + "diveLog_computerTissue_labelN2": "N₂", + "diveLog_computerTissue_labelHe": "He" } diff --git a/lib/l10n/arb/app_zh.arb b/lib/l10n/arb/app_zh.arb index 8b2f77d618..2613b594aa 100644 --- a/lib/l10n/arb/app_zh.arb +++ b/lib/l10n/arb/app_zh.arb @@ -12504,5 +12504,22 @@ "diveCenters_rental_leadAdjustmentLabel": "需额外配重({unit})", "diveCenters_rental_volumeLabel": "实际容量({unit})", "diveCenters_rental_noteLabel": "备注", - "diveCenters_rental_deleteConfirm": "删除此租赁备注?" + "diveCenters_rental_deleteConfirm": "删除此租赁备注?", + "diveLog_computerTissue_title": "潜水电脑", + "diveLog_computerTissue_algorithm": "算法", + "diveLog_computerTissue_compartments": "{count} 个组织舱", + "diveLog_computerTissue_compartmentsWithAlgorithm": "{count} 个 {algorithm} 组织舱", + "diveLog_computerTissue_compartment": "C{number}", + "diveLog_computerTissue_endLoading": "潜水结束时的负荷", + "diveLog_computerTissue_endTensions": "潜水结束时的组织张力", + "diveLog_computerTissue_columnStart": "开始", + "diveLog_computerTissue_columnEnd": "结束", + "diveLog_computerTissue_rowN2Load": "N₂ 负荷", + "diveLog_computerTissue_rowCns": "CNS", + "diveLog_computerTissue_rowOtu": "OTU", + "diveLog_computerTissue_rgbmNitrogen": "RGBM N₂ 系数", + "diveLog_computerTissue_rgbmHelium": "RGBM He 系数", + "diveLog_computerTissue_calculated": "计算值 {value}", + "diveLog_computerTissue_labelN2": "N₂", + "diveLog_computerTissue_labelHe": "He" } diff --git a/test/core/constants/profile_metrics_test.dart b/test/core/constants/profile_metrics_test.dart index 0141851b63..cfa5f1ab09 100644 --- a/test/core/constants/profile_metrics_test.dart +++ b/test/core/constants/profile_metrics_test.dart @@ -37,8 +37,10 @@ void main() { cnsActual: MetricDataSource.calculated, decoStopActual: MetricDataSource.calculated, gtrActual: MetricDataSource.computer, + gf99Actual: MetricDataSource.computer, ); expect(info.ndlActual, MetricDataSource.computer); + expect(info.gf99Actual, MetricDataSource.computer); expect(info.gtrActual, MetricDataSource.computer); expect(info.ceilingActual, MetricDataSource.calculated); expect(info.ttsActual, MetricDataSource.computer); @@ -54,9 +56,11 @@ void main() { cnsActual: MetricDataSource.calculated, decoStopActual: MetricDataSource.calculated, gtrActual: MetricDataSource.calculated, + gf99Actual: MetricDataSource.calculated, ); expect(info.ndlActual, MetricDataSource.calculated); expect(info.gtrActual, MetricDataSource.calculated); + expect(info.gf99Actual, MetricDataSource.calculated); }); }); diff --git a/test/core/database/migration_v225_computer_tissue_test.dart b/test/core/database/migration_v225_computer_tissue_test.dart new file mode 100644 index 0000000000..e80756cb7e --- /dev/null +++ b/test/core/database/migration_v225_computer_tissue_test.dart @@ -0,0 +1,90 @@ +import 'package:drift/native.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:submersion/core/database/database.dart'; + +const _column = 'computer_tissue_json'; + +void main() { + test('v225 is at or below the current schema version and in the ladder', () { + // Relaxed once v226 (media cloud asset id) landed on top; the newest + // rung owns the exact assertion. + expect(AppDatabase.currentSchemaVersion, greaterThanOrEqualTo(225)); + expect(AppDatabase.migrationVersions, contains(225)); + }); + + test('the column is additive, so the sync floor does not move', () { + expect(AppDatabase.minimumCompatibleSchemaVersion, 224); + }); + + test('a fresh database has the nullable computer tissue column', () async { + final db = AppDatabase(NativeDatabase.memory()); + addTearDown(db.close); + + final cols = await db.customSelect("PRAGMA table_info('dives')").get(); + final byName = {for (final c in cols) c.read('name'): c}; + expect(byName, contains(_column)); + expect(byName[_column]!.read('notnull'), 0); + expect(byName[_column]!.read('type'), 'TEXT'); + }); + + test( + 'a database stranded before v225 gains the column via beforeOpen', + () async { + final nativeDb = NativeDatabase.memory( + setup: (rawDb) { + rawDb.execute(''' + CREATE TABLE dives ( + id TEXT NOT NULL PRIMARY KEY, + dive_date_time INTEGER NOT NULL, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL + ) + '''); + }, + ); + final db = AppDatabase(nativeDb); + addTearDown(db.close); + + final cols = await db.customSelect("PRAGMA table_info('dives')").get(); + final names = cols.map((c) => c.read('name')).toSet(); + expect(names, contains(_column)); + }, + ); + + test('a v224 database upgrades and gains the column', () async { + final nativeDb = NativeDatabase.memory( + setup: (rawDb) { + rawDb.execute('PRAGMA user_version = 224'); + rawDb.execute(''' + CREATE TABLE dives ( + id TEXT NOT NULL PRIMARY KEY, + dive_date_time INTEGER NOT NULL, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL + ) + '''); + }, + ); + final db = AppDatabase(nativeDb); + addTearDown(db.close); + + final cols = await db.customSelect("PRAGMA table_info('dives')").get(); + final names = cols.map((c) => c.read('name')).toSet(); + expect(names, contains(_column)); + final version = await db.customSelect('PRAGMA user_version').getSingle(); + expect(version.read('user_version'), AppDatabase.currentSchemaVersion); + }); + + test('the assert is a no-op when the table is absent', () async { + final nativeDb = NativeDatabase.memory( + setup: (rawDb) { + rawDb.execute('CREATE TABLE unrelated (id TEXT)'); + }, + ); + final db = AppDatabase(nativeDb); + addTearDown(db.close); + + await db.customSelect('SELECT 1').get(); + }); +} diff --git a/test/core/database/profile_series_pack_test.dart b/test/core/database/profile_series_pack_test.dart index 6f2a229574..8fb8fa95fc 100644 --- a/test/core/database/profile_series_pack_test.dart +++ b/test/core/database/profile_series_pack_test.dart @@ -90,7 +90,7 @@ void main() { expect(c1['has_deco_type'], 1); expect(c1['has_deco_stop'], 1); expect(c1['has_positive_ceiling'], 1); - expect(c1['codec_version'], 1); + expect(c1['codec_version'], ProfileSeriesCodec.version); expect(c1['created_at'], 1700000000000); expect(c1['updated_at'], 1700000000000); final decoded = codec.decode(c1['samples'] as dynamic); diff --git a/test/core/services/export/uddf/uddf_gradient_factor_export_test.dart b/test/core/services/export/uddf/uddf_gradient_factor_export_test.dart new file mode 100644 index 0000000000..40af47c460 --- /dev/null +++ b/test/core/services/export/uddf/uddf_gradient_factor_export_test.dart @@ -0,0 +1,79 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:submersion/core/services/export/uddf/uddf_export_service.dart'; +import 'package:submersion/core/services/export/uddf/uddf_full_export_service.dart'; +import 'package:submersion/core/services/export/uddf/uddf_full_import_service.dart'; +import 'package:submersion/features/dive_log/domain/entities/dive.dart'; +import 'package:xml/xml.dart'; + +/// A sample's computer-reported GF99 goes out as the waypoint's +/// `` (UDDF 3.2), so a backup and restore keeps it. +/// +/// Covers both waypoint writers: the shared one in +/// `uddf_export_builders.dart` used by the full backup, and the dives-only +/// writer in `uddf_export_service.dart`. +void main() { + Dive dive({List profile = const []}) => Dive( + id: 'dive-a', + diveNumber: 1, + dateTime: DateTime.utc(2026, 3, 1, 9), + bottomTime: const Duration(minutes: 45), + maxDepth: 25.0, + avgDepth: 18.0, + tanks: const [DiveTank(id: 'tank-a', gasMix: GasMix(o2: 32))], + profile: profile, + ); + + const recorded = [ + DiveProfilePoint(timestamp: 10, depth: 4.5, gf99: 0), + DiveProfilePoint(timestamp: 60, depth: 12.0), + DiveProfilePoint(timestamp: 120, depth: 6.0, gf99: 63), + DiveProfilePoint(timestamp: 180, depth: 3.0, gf99: 104), + ]; + + final writers = Function(Dive)>{ + 'full backup': (d) => + UddfFullExportService().generateAllDataXmlForTest(dives: [d]), + 'dives-only export': (d) => + UddfExportService().generateDivesUddfContent([d]), + }; + + List waypoints(String xml) => XmlDocument.parse( + xml, + ).findAllElements('dive').single.findAllElements('waypoint').toList(); + + for (final MapEntry(key: name, value: write) in writers.entries) { + group(name, () { + test('writes gradientfactor only on samples that have a GF99', () async { + final points = waypoints(await write(dive(profile: recorded))); + + expect(points.map((w) => w.getElement('gradientfactor')?.innerText), [ + '0', + null, + '63', + '104', + ]); + }); + + test('writes gradientfactor as a direct child of the waypoint', () async { + final points = waypoints(await write(dive(profile: recorded))); + + for (final waypoint in points) { + expect( + waypoint.findAllElements('gradientfactor').length, + waypoint.findElements('gradientfactor').length, + ); + } + }); + + test('restores the GF99 through the UDDF reader', () async { + final xml = await write(dive(profile: recorded)); + final result = await UddfFullImportService().importAllDataFromUddf(xml); + + final profile = + result.dives.single['profile'] as List>; + expect(profile.map((point) => point['gf99']), [0, null, 63, 104]); + expect(profile[1].containsKey('gf99'), isFalse); + }); + }); + } +} diff --git a/test/core/services/export/uddf/uddf_gradient_factor_import_test.dart b/test/core/services/export/uddf/uddf_gradient_factor_import_test.dart new file mode 100644 index 0000000000..97dcc62973 --- /dev/null +++ b/test/core/services/export/uddf/uddf_gradient_factor_import_test.dart @@ -0,0 +1,110 @@ +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:submersion/core/services/export/uddf/uddf_full_import_service.dart'; +import 'package:submersion/core/services/export/uddf/uddf_import_service.dart'; + +/// Shearwater Cloud (and Subsurface) write the computer's GF99 as a +/// `` inside each ``. The readers carry it on the +/// sample as `gf99`, a whole percent, and only when the waypoint has one. +const _perdix2 = 'test/dives/005_oc-trimix-two-deco-gases--perdix2.uddf'; +const _petrel3 = 'test/dives/006_ccr_petrel3_shearwater-cloud-export.uddf'; +const _oceanic = 'test/dives/issue_279_oceanic_plus_export.uddf'; + +Future>>> _fullImportProfiles( + String path, +) async { + final result = await UddfFullImportService().importAllDataFromUddf( + File(path).readAsStringSync(), + ); + expect(result.dives, isNotEmpty); + return result.dives + .map((dive) => dive['profile'] as List>) + .toList(); +} + +Future>> _fullImportProfile(String path) async { + final profiles = await _fullImportProfiles(path); + expect(profiles, hasLength(1)); + return profiles.single; +} + +List _gf99Values(List> profile) => + profile.map((point) => point['gf99']).whereType().toList(); + +void _expectPlausibleGf99(List> profile) { + final values = _gf99Values(profile); + expect(values.length, greaterThan(100)); + expect(values.every((value) => value >= 0 && value <= 200), isTrue); + expect(profile.every((point) => point['gf99'] is int?), isTrue); +} + +void main() { + group('UddfFullImportService waypoint gradientfactor', () { + test('Perdix 2 OC trimix export carries GF99 on its samples', () async { + final profile = await _fullImportProfile(_perdix2); + + _expectPlausibleGf99(profile); + // The file ends on 63; the trailing + // waypoints that have none carry no key at all rather than a null. + expect(_gf99Values(profile).last, 63); + expect(profile.first['gf99'], 0); + }); + + test('Petrel 3 CCR export carries GF99 on its samples', () async { + final profile = await _fullImportProfile(_petrel3); + + _expectPlausibleGf99(profile); + // 195 of the 385 waypoints have a . + expect(_gf99Values(profile), hasLength(195)); + expect(_gf99Values(profile).last, 59); + }); + + test('a waypoint without gradientfactor gets no gf99 key', () async { + final profile = await _fullImportProfile(_perdix2); + + // The second waypoint (divetime 10) has nodecotime but no + // gradientfactor. + final second = profile.firstWhere((point) => point['timestamp'] == 10); + expect(second.containsKey('gf99'), isFalse); + }); + + test('Oceanic+ export, which has none, yields no gf99 keys', () async { + final profiles = await _fullImportProfiles(_oceanic); + + // Nine dives, none with a . + expect(profiles, hasLength(9)); + expect(profiles.every((profile) => profile.isNotEmpty), isTrue); + expect( + profiles.any( + (profile) => profile.any((point) => point.containsKey('gf99')), + ), + isFalse, + ); + }); + + test('leaves the dive-level computer tissue absent', () async { + final result = await UddfFullImportService().importAllDataFromUddf( + File(_petrel3).readAsStringSync(), + ); + + // The file carries no explicit end-of-dive tissue state; the + // per-sample GF99 trace is the only tissue data it has. + expect(result.dives.single.containsKey('computerTissue'), isFalse); + }); + }); + + group('UddfImportService waypoint gradientfactor', () { + test('Petrel 3 CCR export carries GF99 on its samples', () async { + final result = await UddfImportService().importDivesFromUddf( + File(_petrel3).readAsStringSync(), + ); + final dives = result['dives']!; + expect(dives, hasLength(1)); + final profile = dives.single['profile'] as List>; + + _expectPlausibleGf99(profile); + expect(_gf99Values(profile).last, 59); + }); + }); +} diff --git a/test/core/services/export/uddf/uddf_gradient_factor_parse_test.dart b/test/core/services/export/uddf/uddf_gradient_factor_parse_test.dart new file mode 100644 index 0000000000..6fa57b0fb1 --- /dev/null +++ b/test/core/services/export/uddf/uddf_gradient_factor_parse_test.dart @@ -0,0 +1,44 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:submersion/core/services/export/uddf/uddf_gradient_factor.dart'; + +/// The per-waypoint `` is GF99 as a whole percent. Shearwater +/// Cloud and Subsurface write integers; a few writers use a fraction of one. +void main() { + group('parseUddfGradientFactorPercent', () { + test('reads whole percents as written', () { + expect(parseUddfGradientFactorPercent('0'), 0); + expect(parseUddfGradientFactorPercent('63'), 63); + expect(parseUddfGradientFactorPercent(' 59 '), 59); + }); + + test('does not clamp a supersaturated value', () { + expect(parseUddfGradientFactorPercent('120'), 120); + expect(parseUddfGradientFactorPercent('120.4'), 120); + }); + + test('scales a fraction of one to percent', () { + expect(parseUddfGradientFactorPercent('0.63'), 63); + expect(parseUddfGradientFactorPercent('0.5'), 50); + expect(parseUddfGradientFactorPercent('1.0'), 100); + }); + + test('rounds a decimal percent to the nearest whole percent', () { + expect(parseUddfGradientFactorPercent('63.4'), 63); + expect(parseUddfGradientFactorPercent('63.6'), 64); + expect(parseUddfGradientFactorPercent('0.0'), 0); + }); + + test('keeps an integer one as one percent, not a fraction', () { + expect(parseUddfGradientFactorPercent('1'), 1); + }); + + test('reads blank and non-numeric text as absent', () { + expect(parseUddfGradientFactorPercent(null), isNull); + expect(parseUddfGradientFactorPercent(''), isNull); + expect(parseUddfGradientFactorPercent(' '), isNull); + expect(parseUddfGradientFactorPercent('n/a'), isNull); + expect(parseUddfGradientFactorPercent('NaN'), isNull); + expect(parseUddfGradientFactorPercent('Infinity'), isNull); + }); + }); +} diff --git a/test/core/services/suunto_cloud/suunto_dive_parser_test.dart b/test/core/services/suunto_cloud/suunto_dive_parser_test.dart index 221c8a6821..1e7e88aa6d 100644 --- a/test/core/services/suunto_cloud/suunto_dive_parser_test.dart +++ b/test/core/services/suunto_cloud/suunto_dive_parser_test.dart @@ -926,4 +926,79 @@ void main() { expect(result.dive.entryLongitude, isNull); }); }); + + group('computer tissue', () { + Map headerWithDiving(Map diving) => { + ..._header(), + 'Diving': {..._header()['Diving'] as Map, ...diving}, + }; + + test('builds a tissue snapshot from a DeviceLog EON Core header', () { + final result = SuuntoDiveParser.parse( + header: headerWithDiving({ + 'Algorithm': 'Suunto Fused2 RGBM', + 'StartTissue': { + 'Nitrogen': [ + 79000, 79000, 79000, 79000, 79000, 79000, 79008, 79123, // + 79457, 80568, 81755, 82776, 83604, 84269, 85254, + ], + 'Helium': List.filled(15, 0), + }, + 'EndTissue': { + 'Nitrogen': [ + 89665, 97105, 116432, 135968, 142849, 131069, 113059, 103999, // + 98930, 94002, 91918, 90918, 90376, 90058, 89736, + ], + 'Helium': List.filled(15, 0), + 'CNS': 0.132, + 'OTU': 35.57, + 'RgbmNitrogen': 0.98, + 'RgbmHelium': 0.985, + }, + }), + samples: const [], + ); + + final snapshot = result.computerTissue; + expect(snapshot, isNotNull); + expect(snapshot!.algorithm, 'Suunto Fused2 RGBM'); + expect(snapshot.start!.n2Bar, hasLength(15)); + expect(snapshot.start!.n2Bar!.first, 0.79); + expect(snapshot.end!.n2Bar![4], 1.42849); + expect(snapshot.end!.cnsPercent, closeTo(13.2, 1e-9)); + expect(snapshot.end!.otu, 35.57); + expect(snapshot.end!.rgbmNitrogen, 0.98); + // The import pipeline only ever sees the DownloadedDive, so the + // snapshot has to ride on it too. + expect(result.dive.computerTissue, snapshot); + }); + + test('reads the HelO2 Pressure-list encoding', () { + final result = SuuntoDiveParser.parse( + header: headerWithDiving({ + 'Algorithm': 'Suunto Technical RGBM', + 'StartTissue': { + 'Nitrogen': {'Pressure': List.filled(9, 79000)}, + }, + 'EndTissue': {'OLF': 0.05, 'CNS': 0.04, 'OTU': 12.0}, + }), + samples: const [], + ); + + expect(result.computerTissue!.start!.n2Bar, hasLength(9)); + expect(result.computerTissue!.end!.cnsPercent, closeTo(4.0, 1e-9)); + }); + + test('leaves the snapshot null for a header without tissue data', () { + final result = SuuntoDiveParser.parse(header: _header(), samples: []); + expect(result.computerTissue, isNull); + expect(result.dive.computerTissue, isNull); + }); + + test('leaves the snapshot null for a header without Diving', () { + final header = _header()..remove('Diving'); + final result = SuuntoDiveParser.parse(header: header, samples: []); + expect(result.computerTissue, isNull); + }); + }); } diff --git a/test/core/services/suunto_cloud/suunto_sml_normalizer_test.dart b/test/core/services/suunto_cloud/suunto_sml_normalizer_test.dart index 60f2e30105..2277509d16 100644 --- a/test/core/services/suunto_cloud/suunto_sml_normalizer_test.dart +++ b/test/core/services/suunto_cloud/suunto_sml_normalizer_test.dart @@ -2,7 +2,9 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:submersion/core/services/suunto_cloud/suunto_api_exception.dart'; import 'package:submersion/core/services/suunto_cloud/suunto_sml_normalizer.dart'; -Map _cloudFixture() => { +Map _cloudFixture({ + Map diveHeaderExtra = const {}, +}) => { 'Summary': { 'Samples': [ { @@ -41,6 +43,7 @@ Map _cloudFixture() => { ], 'LowGf': 30, 'HighGf': 85, + ...diveHeaderExtra, }, }, }, @@ -203,4 +206,68 @@ void main() { ); }); }); + + group('SuuntoSmlNormalizer.parse (cloud shape tissue state)', () { + test('hoists StartTissue, EndTissue and Algorithm onto Diving', () { + final export = SuuntoSmlNormalizer.parse( + _cloudFixture( + diveHeaderExtra: { + 'Algorithm': 'Suunto Fused2 RGBM', + 'StartTissue': { + 'Nitrogen': [79000, 79000], + }, + 'EndTissue': { + 'Nitrogen': [89665, 97105], + 'CNS': 0.132, + }, + }, + ), + ); + + final diving = export.header['Diving'] as Map; + expect(diving['Algorithm'], 'Suunto Fused2 RGBM'); + expect(diving['StartTissue'], { + 'Nitrogen': [79000, 79000], + }); + expect(diving['EndTissue'], { + 'Nitrogen': [89665, 97105], + 'CNS': 0.132, + }); + // The existing hoists still ride along. + expect(diving['GfLow'], 30); + }); + + test('leaves the tissue keys absent when the DiveHeader has none', () { + final export = SuuntoSmlNormalizer.parse(_cloudFixture()); + final diving = export.header['Diving'] as Map; + expect(diving.containsKey('StartTissue'), isFalse); + expect(diving.containsKey('EndTissue'), isFalse); + expect(diving.containsKey('Algorithm'), isFalse); + }); + + test('keeps the DeviceLog shape Diving block as-is', () { + final json = { + 'DeviceLog': { + 'Header': { + 'ActivityType': 51, + 'DateTime': '2024-05-01T10:00:00Z', + 'Diving': { + 'Algorithm': 'Suunto Fused2 RGBM', + 'StartTissue': { + 'Nitrogen': [79000], + }, + }, + }, + 'Samples': >[], + }, + }; + + final export = SuuntoSmlNormalizer.parse(json); + final diving = export.header['Diving'] as Map; + expect(diving['Algorithm'], 'Suunto Fused2 RGBM'); + expect(diving['StartTissue'], { + 'Nitrogen': [79000], + }); + }); + }); } diff --git a/test/core/services/suunto_cloud/suunto_tissue_parser_test.dart b/test/core/services/suunto_cloud/suunto_tissue_parser_test.dart new file mode 100644 index 0000000000..e23d9b7fa4 --- /dev/null +++ b/test/core/services/suunto_cloud/suunto_tissue_parser_test.dart @@ -0,0 +1,209 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:submersion/core/services/suunto_cloud/suunto_tissue_parser.dart'; + +// Real EON Core (Fused2 RGBM, 15 compartments) values, in Pascal. +const _eonStartN2 = [ + 79000, + 79000, + 79000, + 79000, + 79000, + 79000, + 79008, + 79123, + 79457, + 80568, + 81755, + 82776, + 83604, + 84269, + 85254, +]; +const _eonEndN2 = [ + 89665, + 97105, + 116432, + 135968, + 142849, + 131069, + 113059, + 103999, + 98930, + 94002, + 91918, + 90918, + 90376, + 90058, + 89736, +]; + +Map _eonCoreDiving() => { + 'Algorithm': 'Suunto Fused2 RGBM', + 'DesaturationTime': 61200, + 'StartTissue': { + 'Nitrogen': _eonStartN2, + 'Helium': List.filled(15, 0), + 'RgbmNitrogen': 1.0, + 'RgbmHelium': 1.0, + 'CNS': 0.0, + 'OTU': 0.0, + }, + 'EndTissue': { + 'Nitrogen': _eonEndN2, + 'Helium': List.filled(15, 0), + 'RgbmNitrogen': 0.98, + 'RgbmHelium': 0.985, + 'CNS': 0.132, + 'OTU': 35.57, + }, +}; + +void main() { + group('parseSuuntoTissue (DeviceLog array encoding)', () { + test('converts an EON Core header into a snapshot', () { + final snapshot = parseSuuntoTissue(_eonCoreDiving()); + + expect(snapshot, isNotNull); + expect(snapshot!.algorithm, 'Suunto Fused2 RGBM'); + + final start = snapshot.start!; + expect(start.n2Bar, hasLength(15)); + expect(start.n2Bar!.first, 0.79); + expect(start.n2Bar!.last, 0.85254); + expect(start.heBar, hasLength(15)); + expect(start.heBar!.every((v) => v == 0), isTrue); + + final end = snapshot.end!; + expect(end.n2Bar, hasLength(15)); + expect(end.n2Bar![4], 1.42849); + expect(end.cnsPercent, closeTo(13.2, 1e-9)); + expect(end.otu, 35.57); + expect(end.rgbmNitrogen, 0.98); + expect(end.rgbmHelium, 0.985); + }); + + test('keeps five decimals when converting Pascal to bar', () { + final snapshot = parseSuuntoTissue({ + 'EndTissue': { + 'Nitrogen': [142849.4], + }, + }); + + expect(snapshot!.end!.n2Bar, [1.42849]); + }); + + test('leaves fields the source did not carry as null', () { + final snapshot = parseSuuntoTissue(_eonCoreDiving()); + + expect(snapshot!.end!.loadPercent, isNull); + expect(snapshot.end!.n2LoadPercent, isNull); + expect(snapshot.end!.gf99Percent, isNull); + expect(snapshot.end!.surfaceGfPercent, isNull); + }); + }); + + group('parseSuuntoTissue (SML-XML derived encodings)', () { + test('reads a Nitrogen object holding a Pressure list', () { + final snapshot = parseSuuntoTissue({ + 'Algorithm': 'Suunto Technical RGBM', + 'StartTissue': { + 'Nitrogen': {'Pressure': List.filled(9, 79000)}, + 'Helium': {'Pressure': List.filled(9, 0)}, + }, + 'EndTissue': { + 'OLF': 0.05, + 'CNS': 0.04, + 'OTU': 12.0, + 'RgbmNitrogen': 0.9, + 'RgbmHelium': 1.0, + }, + }); + + expect(snapshot, isNotNull); + expect(snapshot!.algorithm, 'Suunto Technical RGBM'); + expect(snapshot.start!.n2Bar, hasLength(9)); + expect(snapshot.start!.n2Bar!.first, 0.79); + expect(snapshot.start!.heBar, hasLength(9)); + + // The HelO2 end state has no compartment arrays, only the summaries. + final end = snapshot.end!; + expect(end.n2Bar, isNull); + expect(end.hasCompartments, isFalse); + expect(end.cnsPercent, closeTo(4.0, 1e-9)); + expect(end.otu, 12.0); + expect(end.rgbmNitrogen, 0.9); + expect(end.rgbmHelium, 1.0); + }); + + test('reads a list of Pressure maps', () { + final snapshot = parseSuuntoTissue({ + 'StartTissue': { + 'Nitrogen': [ + {'Pressure': 79000}, + {'Pressure': 80000}, + ], + }, + }); + + expect(snapshot!.start!.n2Bar, [0.79, 0.8]); + }); + + test('falls back to OLF for the CNS percentage when CNS is absent', () { + final snapshot = parseSuuntoTissue({ + 'EndTissue': {'OLF': 0.25}, + }); + + expect(snapshot!.end!.cnsPercent, closeTo(25.0, 1e-9)); + }); + }); + + group('parseSuuntoTissue (absent or malformed data)', () { + test('returns null for a header without tissue state', () { + expect(parseSuuntoTissue({'GfLow': 30, 'GfHigh': 85}), isNull); + }); + + test('returns null when only the algorithm name is present', () { + expect(parseSuuntoTissue({'Algorithm': 'Suunto Fused2 RGBM'}), isNull); + }); + + test('skips a state with no numeric content', () { + final snapshot = parseSuuntoTissue({ + 'Algorithm': 'Suunto Fused2 RGBM', + 'StartTissue': {'Nitrogen': [], 'Helium': 'n/a'}, + 'EndTissue': { + 'Nitrogen': [89665], + }, + }); + + expect(snapshot, isNotNull); + expect(snapshot!.start, isNull); + expect(snapshot.end!.n2Bar, [0.89665]); + }); + + test('drops a compartment list with a non-numeric element', () { + final snapshot = parseSuuntoTissue({ + 'EndTissue': { + 'Nitrogen': [89665, 'x', 90000], + 'CNS': 0.1, + }, + }); + + expect(snapshot!.end!.n2Bar, isNull); + expect(snapshot.end!.cnsPercent, closeTo(10.0, 1e-9)); + }); + + test('ignores a non-map tissue entry and a non-string algorithm', () { + final snapshot = parseSuuntoTissue({ + 'Algorithm': 42, + 'StartTissue': 'bogus', + 'EndTissue': { + 'Nitrogen': [89665], + }, + }); + + expect(snapshot!.algorithm, isNull); + expect(snapshot.start, isNull); + expect(snapshot.end!.n2Bar, [0.89665]); + }); + }); +} diff --git a/test/core/services/sync/series_forward_version_test.dart b/test/core/services/sync/series_forward_version_test.dart index b8b00dd7a0..99085d9802 100644 --- a/test/core/services/sync/series_forward_version_test.dart +++ b/test/core/services/sync/series_forward_version_test.dart @@ -37,15 +37,15 @@ void main() { }); tearDown(tearDownTestDatabase); - /// A blob written by a hypothetical v2 codec: same field table, version - /// byte 2, which this build has no table for. + /// A blob written by a hypothetical v3 codec: the v1 field table, version + /// byte 3, which this build has no table for. Uint8List futureBlob() => - const ProfileSeriesCodec(fieldTables: {2: kProfileFieldTableV1}).encode( + const ProfileSeriesCodec(fieldTables: {3: kProfileFieldTableV1}).encode( const [ ProfileSample(timestamp: 0, depth: 1.0), ProfileSample(timestamp: 60, depth: 9.0), ], - version: 2, + version: 3, ).bytes; test('the codec names a forward version as such', () { @@ -53,7 +53,7 @@ void main() { const ProfileSeriesCodec().decode(futureBlob()); fail('expected a refusal'); } on UnknownSeriesVersionException catch (e) { - expect(e.blobVersion, 2); + expect(e.blobVersion, 3); expect(e.isForwardVersion, isTrue); } }); @@ -73,7 +73,7 @@ void main() { 'hasDecoType': false, 'hasDecoStop': false, 'hasPositiveCeiling': false, - 'codecVersion': 2, + 'codecVersion': 3, 'samples': base64Encode(bytes), 'createdAt': now, 'updatedAt': now, diff --git a/test/features/dive_computer/data/services/dive_import_service_test.dart b/test/features/dive_computer/data/services/dive_import_service_test.dart index 8d10da3bc5..5bc4086709 100644 --- a/test/features/dive_computer/data/services/dive_import_service_test.dart +++ b/test/features/dive_computer/data/services/dive_import_service_test.dart @@ -7,6 +7,7 @@ import 'package:submersion/features/dive_computer/data/services/dive_import_serv import 'package:submersion/features/dive_computer/domain/entities/downloaded_dive.dart'; import 'package:submersion/features/dive_log/data/repositories/dive_computer_repository_impl.dart'; import 'package:submersion/features/dive_log/data/repositories/dive_repository_impl.dart'; +import 'package:submersion/features/dive_log/domain/entities/computer_tissue_snapshot.dart'; import 'package:submersion/features/dive_log/domain/entities/profile_series.dart'; import 'package:submersion/features/dive_log/domain/codecs/profile_sample.dart' as codec; @@ -75,6 +76,7 @@ void main() { gfLow: anyNamed('gfLow'), gfHigh: anyNamed('gfHigh'), decoConservatism: anyNamed('decoConservatism'), + computerTissue: anyNamed('computerTissue'), events: anyNamed('events'), gasSwitches: anyNamed('gasSwitches'), diveNumber: anyNamed('diveNumber'), @@ -89,6 +91,93 @@ void main() { ).thenAnswer((_) async => 'dive-id'); }); + group('computer tissue', () { + const snapshot = ComputerTissueSnapshot( + algorithm: 'Suunto Fused2 RGBM', + start: ComputerTissueState(n2Bar: [0.79, 0.79]), + end: ComputerTissueState(n2Bar: [0.9, 1.1], cnsPercent: 13.2), + ); + final dive = DownloadedDive( + fingerprint: 'fp-tissue', + startTime: DateTime(2026, 4, 1, 9, 0), + durationSeconds: 1800, + maxDepth: 18.0, + profile: const [], + computerTissue: snapshot, + ); + + ComputerTissueSnapshot? capturedTissue() => + verify( + mockComputerRepo.importProfile( + computerId: anyNamed('computerId'), + profileStartTime: anyNamed('profileStartTime'), + points: anyNamed('points'), + durationSeconds: anyNamed('durationSeconds'), + maxDepth: anyNamed('maxDepth'), + avgDepth: anyNamed('avgDepth'), + isPrimary: anyNamed('isPrimary'), + diverId: anyNamed('diverId'), + tanks: anyNamed('tanks'), + decoAlgorithm: anyNamed('decoAlgorithm'), + gfLow: anyNamed('gfLow'), + gfHigh: anyNamed('gfHigh'), + decoConservatism: anyNamed('decoConservatism'), + computerTissue: captureAnyNamed('computerTissue'), + events: anyNamed('events'), + gasSwitches: anyNamed('gasSwitches'), + diveNumber: anyNamed('diveNumber'), + forceNew: anyNamed('forceNew'), + rawData: anyNamed('rawData'), + rawFingerprint: anyNamed('rawFingerprint'), + descriptorVendor: anyNamed('descriptorVendor'), + descriptorProduct: anyNamed('descriptorProduct'), + descriptorModel: anyNamed('descriptorModel'), + libdivecomputerVersion: anyNamed('libdivecomputerVersion'), + ), + ).captured.single + as ComputerTissueSnapshot?; + + test('a new dive hands the snapshot to the repository', () async { + when( + mockDiveRepo.getDiveNumberForDate( + any, + diverId: anyNamed('diverId'), + startFrom: anyNamed('startFrom'), + ), + ).thenAnswer((_) async => 1); + + await service.importSingleDiveAsNew(dive, computerId: computer.id); + + expect(capturedTissue(), snapshot); + }); + + test('replacing an existing source hands the snapshot over too', () async { + when( + mockComputerRepo.clearSourceAndProfiles( + diveId: anyNamed('diveId'), + computerId: anyNamed('computerId'), + ), + ).thenAnswer((_) async {}); + final conflict = ImportConflict( + downloaded: dive, + existingDiveId: 'existing-dive-1', + duplicateResult: const DuplicateResult( + matchingDiveId: 'existing-dive-1', + confidence: DuplicateConfidence.exact, + score: 0.95, + ), + ); + + await service.resolveConflict( + conflict, + ConflictResolution.replaceSource, + computer.id, + ); + + expect(capturedTissue(), snapshot); + }); + }); + group('DiveImportService dive numbering', () { test('assigns sequential dive numbers in chronological order', () async { final dive1 = DownloadedDive( diff --git a/test/features/dive_computer/domain/entities/downloaded_dive_test.dart b/test/features/dive_computer/domain/entities/downloaded_dive_test.dart new file mode 100644 index 0000000000..40963f6ef4 --- /dev/null +++ b/test/features/dive_computer/domain/entities/downloaded_dive_test.dart @@ -0,0 +1,33 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:submersion/features/dive_computer/domain/entities/downloaded_dive.dart'; +import 'package:submersion/features/dive_log/domain/entities/computer_tissue_snapshot.dart'; + +void main() { + group('DownloadedDive.computerTissue', () { + DownloadedDive makeDive({ComputerTissueSnapshot? computerTissue}) => + DownloadedDive( + startTime: DateTime.utc(2026, 3, 15, 10, 32), + durationSeconds: 1800, + maxDepth: 18.5, + profile: const [], + computerTissue: computerTissue, + ); + + test('is null when the computer reported no tissue state', () { + expect(makeDive().computerTissue, isNull); + }); + + test('carries the snapshot the parser attached', () { + const snapshot = ComputerTissueSnapshot( + algorithm: 'Suunto Fused2 RGBM', + start: ComputerTissueState(n2Bar: [0.79, 0.79]), + end: ComputerTissueState(n2Bar: [0.9, 1.1], cnsPercent: 13.2), + ); + + final dive = makeDive(computerTissue: snapshot); + + expect(dive.computerTissue, snapshot); + expect(dive.computerTissue!.hasCompartmentData, isTrue); + }); + }); +} diff --git a/test/features/dive_import/data/services/fit/fit_profile_extractor_test.dart b/test/features/dive_import/data/services/fit/fit_profile_extractor_test.dart index 0c5cc730b9..2e0fd4cf1f 100644 --- a/test/features/dive_import/data/services/fit/fit_profile_extractor_test.dart +++ b/test/features/dive_import/data/services/fit/fit_profile_extractor_test.dart @@ -1,5 +1,10 @@ +import 'dart:io'; + import 'package:fit_tool/fit_tool.dart'; +// ignore: implementation_imports +import 'package:fit_tool/src/utils/logger.dart' as fit_log; import 'package:flutter_test/flutter_test.dart'; +import 'package:logger/logger.dart' show Level, Logger; import 'package:submersion/features/dive_import/data/services/fit/fit_profile_extractor.dart'; void main() { @@ -13,7 +18,8 @@ void main() { ..nextStopDepth = 6.0 ..timeToSurface = 480 ..ndlTime = 0 - ..cnsLoad = 15; + ..cnsLoad = 15 + ..n2Load = 42; final r2 = RecordMessage() ..timestamp = start .add(const Duration(seconds: 1)) @@ -29,6 +35,49 @@ void main() { expect(s.ttsSeconds, 480); expect(s.ndlSeconds, 0); expect(s.cns, 15); + expect(s.n2Load, 42); + }); + + test('leaves n2Load null when the record does not carry it', () { + final r = RecordMessage() + ..timestamp = start.millisecondsSinceEpoch + ..depth = 5.0 + ..cnsLoad = 3; + final s = FitProfileExtractor.extract([r]).single; + expect(s.n2Load, isNull); + expect(s.cns, 3); + }); + + test('treats an out-of-range n2Load as absent', () { + final r = RecordMessage() + ..timestamp = start.millisecondsSinceEpoch + ..depth = 5.0 + ..n2Load = 1001; + final s = FitProfileExtractor.extract([r]).single; + expect(s.n2Load, isNull); + }); + + test('reads the recorded N2 loading from a real Descent file', () { + // Silence fit_tool's warnings about fields its bundled profile lacks. + fit_log.logger = Logger(level: Level.error); + final bytes = File( + 'test/dives/005_oc-trimix-two-deco-gases.fit', + ).readAsBytesSync(); + final records = FitFile.fromBytes( + bytes, + ).records.map((r) => r.message).whereType().toList(); + + final samples = FitProfileExtractor.extract(records); + final loads = samples.map((s) => s.n2Load).whereType().toList(); + + // Only the very first record lacks n2_load. The loading peaks well above + // 100 mid-dive (a trimix deco dive) and the final sample matches the + // dive_summary end_n2 of 86. + expect(samples, hasLength(3921)); + expect(loads, hasLength(3920)); + expect(samples.first.n2Load, isNull); + expect(loads.last, 86); + expect(loads.reduce((a, b) => a > b ? a : b), 191); }); test('keeps the record timestamp as Unix ms for later merge alignment', () { diff --git a/test/features/dive_import/data/services/fit/fit_summary_extractor_test.dart b/test/features/dive_import/data/services/fit/fit_summary_extractor_test.dart index 35aebb59cb..a8a69fe988 100644 --- a/test/features/dive_import/data/services/fit/fit_summary_extractor_test.dart +++ b/test/features/dive_import/data/services/fit/fit_summary_extractor_test.dart @@ -1,5 +1,10 @@ +import 'dart:io'; + import 'package:fit_tool/fit_tool.dart'; +// ignore: implementation_imports +import 'package:fit_tool/src/utils/logger.dart' as fit_log; import 'package:flutter_test/flutter_test.dart'; +import 'package:logger/logger.dart' show Level, Logger; import 'package:submersion/features/dive_import/data/services/fit/fit_summary_extractor.dart'; void main() { @@ -12,6 +17,8 @@ void main() { ..surfaceInterval = 167491 ..startCns = 0 ..endCns = 32 + ..startN2 = 5 + ..endN2 = 71 ..o2Toxicity = 90; final session = SessionMessage() ..startPositionLat = 35.815 @@ -35,6 +42,8 @@ void main() { ); // 5168.781 -> rounded expect(s.surfaceInterval, const Duration(seconds: 167491)); expect(s.cnsEnd, 32); + expect(s.startN2, 5); + expect(s.endN2, 71); expect(s.otu, 90); expect(s.entryLat, closeTo(35.815, 1e-4)); expect(s.entryLong, closeTo(14.451, 1e-4)); @@ -55,5 +64,48 @@ void main() { expect(s.bottomTime, isNull); expect(s.entryLat, isNull); expect(s.waterType, isNull); + expect(s.startN2, isNull); + expect(s.endN2, isNull); + }); + + test('leaves N2 null when the summary does not carry it', () { + final s = FitSummaryExtractor.extract( + summary: DiveSummaryMessage() + ..diveNumber = 3 + ..endCns = 12, + session: null, + settings: null, + ); + expect(s.cnsEnd, 12); + expect(s.startN2, isNull); + expect(s.endN2, isNull); + }); + + test('reads start/end N2 from the real Descent dive summary', () { + // Silence fit_tool's warnings about fields its bundled profile lacks. + fit_log.logger = Logger(level: Level.error); + final bytes = File( + 'test/dives/005_oc-trimix-two-deco-gases.fit', + ).readAsBytesSync(); + final messages = FitFile.fromBytes( + bytes, + ).records.map((r) => r.message).whereType().toList(); + // The file holds two dive_summary messages; the one referencing the + // session carries a dive number and the real values. The other is all + // invalid sentinels, which fit_tool reads as null. + final summary = messages.whereType().firstWhere( + (s) => s.diveNumber != null, + ); + final settings = messages.whereType().first; + + final s = FitSummaryExtractor.extract( + summary: summary, + session: null, + settings: settings, + ); + + expect(s.startN2, 0); + expect(s.endN2, 86); + expect(s.decoModel, 'zhl_16c'); }); } diff --git a/test/features/dive_import/data/services/fit/fit_tissue_snapshot_test.dart b/test/features/dive_import/data/services/fit/fit_tissue_snapshot_test.dart new file mode 100644 index 0000000000..e13947dc70 --- /dev/null +++ b/test/features/dive_import/data/services/fit/fit_tissue_snapshot_test.dart @@ -0,0 +1,47 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:submersion/features/dive_import/data/services/fit/fit_summary_extractor.dart'; +import 'package:submersion/features/dive_import/data/services/fit/fit_tissue_snapshot.dart'; +import 'package:submersion/features/dive_log/domain/entities/computer_tissue_snapshot.dart'; + +void main() { + group('fitTissueSnapshot', () { + test('is null when the summary carries no N2 loading', () { + final summary = FitSummary(cnsStart: 0, cnsEnd: 12, decoModel: 'zhl_16c'); + expect(fitTissueSnapshot(summary), isNull); + }); + + test('builds start and end states from N2 and CNS', () { + final summary = FitSummary( + startN2: 0, + endN2: 86, + cnsStart: 1, + cnsEnd: 23, + decoModel: 'zhl_16c', + ); + + expect( + fitTissueSnapshot(summary), + const ComputerTissueSnapshot( + algorithm: 'zhl_16c', + start: ComputerTissueState(n2LoadPercent: 0, cnsPercent: 1), + end: ComputerTissueState(n2LoadPercent: 86, cnsPercent: 23), + ), + ); + }); + + test('leaves a side null when only the other N2 value is present', () { + final onlyEnd = fitTissueSnapshot(FitSummary(endN2: 40, cnsEnd: 5)); + expect(onlyEnd, isNotNull); + expect(onlyEnd!.start, isNull); + expect( + onlyEnd.end, + const ComputerTissueState(n2LoadPercent: 40, cnsPercent: 5), + ); + expect(onlyEnd.algorithm, isNull); + + final onlyStart = fitTissueSnapshot(FitSummary(startN2: 12)); + expect(onlyStart!.start, const ComputerTissueState(n2LoadPercent: 12)); + expect(onlyStart.end, isNull); + }); + }); +} diff --git a/test/features/dive_import/data/services/fit_parser_service_test.dart b/test/features/dive_import/data/services/fit_parser_service_test.dart index d07d40a59a..0692524266 100644 --- a/test/features/dive_import/data/services/fit_parser_service_test.dart +++ b/test/features/dive_import/data/services/fit_parser_service_test.dart @@ -1,9 +1,11 @@ +import 'dart:io'; import 'dart:typed_data'; import 'package:fit_tool/fit_tool.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:submersion/features/dive_import/data/services/fit_parser_service.dart'; import 'package:submersion/features/dive_import/domain/entities/imported_dive.dart'; +import 'package:submersion/features/dive_log/domain/entities/computer_tissue_snapshot.dart'; /// Builds a minimal FIT file representing a dive activity. /// @@ -681,4 +683,35 @@ void main() { expect(results, isEmpty); }); }); + + group('computer tissue', () { + const service = FitParserService(); + + test('is null when the FIT file records no N2 loading', () async { + final dive = await service.parseFitFile(buildRichDiveFitFile()); + expect(dive, isNotNull); + expect(dive!.computerTissue, isNull); + expect(dive.profile.any((s) => s.n2Load != null), isFalse); + }); + + test( + 'reads the Descent N2 loading into the snapshot and samples', + () async { + final bytes = File( + 'test/dives/005_oc-trimix-two-deco-gases.fit', + ).readAsBytesSync(); + final dive = (await service.parseFitFile(bytes))!; + + final tissue = dive.computerTissue; + expect(tissue, isA()); + expect(tissue!.algorithm, 'zhl_16c'); + expect(tissue.start?.n2LoadPercent, 0); + expect(tissue.start?.cnsPercent, dive.cnsStart); + expect(tissue.end?.n2LoadPercent, 86); + expect(tissue.end?.cnsPercent, dive.cnsEnd); + expect(dive.profile.first.n2Load, isNull); + expect(dive.profile.last.n2Load, 86); + }, + ); + }); } diff --git a/test/features/dive_import/domain/entities/imported_dive_test.dart b/test/features/dive_import/domain/entities/imported_dive_test.dart index 00e5454638..47f057f9e8 100644 --- a/test/features/dive_import/domain/entities/imported_dive_test.dart +++ b/test/features/dive_import/domain/entities/imported_dive_test.dart @@ -1,5 +1,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:submersion/features/dive_import/domain/entities/imported_dive.dart'; +import 'package:submersion/features/dive_log/domain/entities/computer_tissue_snapshot.dart'; void main() { group('ImportedDive', () { @@ -56,4 +57,79 @@ void main() { expect(const ImportedGasSwitch(timeSeconds: 0, tankIndex: 0).depth, null); }); }); + + group('computer tissue fields', () { + test('ImportedProfileSample carries gf99 and n2Load', () { + const sample = ImportedProfileSample( + timeSeconds: 60, + depth: 20.0, + gf99: 42, + n2Load: 37, + ); + + expect(sample.gf99, 42); + expect(sample.n2Load, 37); + expect(const ImportedProfileSample(timeSeconds: 0, depth: 0).gf99, null); + expect( + const ImportedProfileSample(timeSeconds: 0, depth: 0).n2Load, + null, + ); + }); + + test('ImportedProfileSample copyWith keeps and overrides gf99/n2Load', () { + const sample = ImportedProfileSample( + timeSeconds: 60, + depth: 20.0, + gf99: 42, + n2Load: 37, + ); + + final kept = sample.copyWith(depth: 21.0); + expect(kept.gf99, 42); + expect(kept.n2Load, 37); + + final changed = sample.copyWith(gf99: 50, n2Load: 40); + expect(changed.gf99, 50); + expect(changed.n2Load, 40); + }); + + test('ImportedProfileSample equality includes gf99 and n2Load', () { + const a = ImportedProfileSample(timeSeconds: 0, depth: 1, gf99: 10); + const b = ImportedProfileSample(timeSeconds: 0, depth: 1, gf99: 10); + const c = ImportedProfileSample(timeSeconds: 0, depth: 1, gf99: 11); + const d = ImportedProfileSample(timeSeconds: 0, depth: 1, n2Load: 11); + + expect(a, b); + expect(a, isNot(c)); + expect(a, isNot(d)); + }); + + test('ImportedDive carries a computer tissue snapshot', () { + const snapshot = ComputerTissueSnapshot( + algorithm: 'Suunto Fused2 RGBM', + end: ComputerTissueState(n2Bar: [0.89665], cnsPercent: 13.2), + ); + final dive = ImportedDive( + sourceId: 'suunto-1', + source: ImportSource.suunto, + startTime: DateTime(2024, 1, 1, 10), + endTime: DateTime(2024, 1, 1, 10, 40), + maxDepth: 30.0, + profile: const [], + computerTissue: snapshot, + ); + final bare = ImportedDive( + sourceId: 'suunto-1', + source: ImportSource.suunto, + startTime: DateTime(2024, 1, 1, 10), + endTime: DateTime(2024, 1, 1, 10, 40), + maxDepth: 30.0, + profile: const [], + ); + + expect(dive.computerTissue, snapshot); + expect(bare.computerTissue, isNull); + expect(dive, isNot(bare)); + }); + }); } diff --git a/test/features/dive_import/domain/services/imported_dive_converter_test.dart b/test/features/dive_import/domain/services/imported_dive_converter_test.dart index f2d9fcee0a..6dbc5649eb 100644 --- a/test/features/dive_import/domain/services/imported_dive_converter_test.dart +++ b/test/features/dive_import/domain/services/imported_dive_converter_test.dart @@ -1,6 +1,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:submersion/features/dive_import/domain/entities/imported_dive.dart'; import 'package:submersion/features/dive_import/domain/services/imported_dive_converter.dart'; +import 'package:submersion/features/dive_log/domain/entities/computer_tissue_snapshot.dart'; void main() { const converter = ImportedDiveConverter(); @@ -307,4 +308,70 @@ void main() { ); }); }); + + group('ImportedDiveConverter computer tissue', () { + test('threads the tissue snapshot onto the Dive', () { + const snapshot = ComputerTissueSnapshot( + algorithm: 'Suunto Fused2 RGBM', + start: ComputerTissueState(n2Bar: [0.79, 0.79]), + end: ComputerTissueState( + n2Bar: [0.89665, 0.97105], + cnsPercent: 13.2, + otu: 35.57, + ), + ); + final importedDive = ImportedDive( + sourceId: 'suunto-tissue', + source: ImportSource.suunto, + startTime: DateTime(2024, 6, 15, 10, 0), + endTime: DateTime(2024, 6, 15, 10, 40), + maxDepth: 30.0, + profile: const [], + computerTissue: snapshot, + ); + + final dive = converter.convert(importedDive); + + expect(dive.computerTissue, snapshot); + }); + + test('leaves computerTissue null when the import had none', () { + final importedDive = ImportedDive( + sourceId: 'no-tissue', + source: ImportSource.garmin, + startTime: DateTime(2024, 6, 15, 10, 0), + endTime: DateTime(2024, 6, 15, 10, 40), + maxDepth: 30.0, + profile: const [], + ); + + expect(converter.convert(importedDive).computerTissue, isNull); + }); + + test('threads gf99 and n2Load onto the profile points', () { + final importedDive = ImportedDive( + sourceId: 'garmin-tissue', + source: ImportSource.garmin, + startTime: DateTime(2024, 6, 15, 10, 0), + endTime: DateTime(2024, 6, 15, 10, 40), + maxDepth: 30.0, + profile: const [ + ImportedProfileSample( + timeSeconds: 0, + depth: 5.0, + gf99: 12, + n2Load: 40, + ), + ImportedProfileSample(timeSeconds: 60, depth: 10.0), + ], + ); + + final dive = converter.convert(importedDive); + + expect(dive.profile[0].gf99, 12); + expect(dive.profile[0].n2Load, 40); + expect(dive.profile[1].gf99, isNull); + expect(dive.profile[1].n2Load, isNull); + }); + }); } diff --git a/test/features/dive_log/data/repositories/dive_computer_repository_tissue_test.dart b/test/features/dive_log/data/repositories/dive_computer_repository_tissue_test.dart new file mode 100644 index 0000000000..48a9a2d1ad --- /dev/null +++ b/test/features/dive_log/data/repositories/dive_computer_repository_tissue_test.dart @@ -0,0 +1,124 @@ +import 'package:drift/drift.dart' hide isNull, isNotNull; +import 'package:flutter_test/flutter_test.dart'; +import 'package:submersion/core/database/database.dart'; +import 'package:submersion/features/dive_log/data/repositories/dive_computer_repository_impl.dart'; +import 'package:submersion/features/dive_log/domain/entities/computer_tissue_snapshot.dart'; + +import '../../../../helpers/test_database.dart'; + +/// Tissue state a dive computer reports for the whole dive (Suunto SML +/// `StartTissue` / `EndTissue`) travels with the download and lands on +/// `dives.computer_tissue_json`; a re-import that carries none must not +/// blank what an earlier import stored. +void main() { + late DiveComputerRepository repository; + late AppDatabase db; + + const computerId = 'suunto-eon-core'; + const points = [ + ProfilePointData(timestamp: 0, depth: 1.0), + ProfilePointData(timestamp: 600, depth: 18.0), + ProfilePointData(timestamp: 1200, depth: 6.0), + ]; + const snapshot = ComputerTissueSnapshot( + algorithm: 'Suunto Fused2 RGBM', + start: ComputerTissueState(n2Bar: [0.79, 0.79, 0.79]), + end: ComputerTissueState( + n2Bar: [0.89665, 0.97105, 1.16432], + cnsPercent: 13.2, + otu: 35.57, + ), + ); + + setUp(() async { + db = await setUpTestDatabase(); + repository = DiveComputerRepository(); + + final now = DateTime.now().millisecondsSinceEpoch; + await db + .into(db.diveComputers) + .insert( + DiveComputersCompanion.insert( + id: computerId, + name: 'Suunto EON Core', + manufacturer: const Value('Suunto'), + model: const Value('EON Core'), + createdAt: now, + updatedAt: now, + ), + ); + }); + + tearDown(() async { + await tearDownTestDatabase(); + }); + + Future storedTissueFor(String diveId) async { + final row = await (db.select( + db.dives, + )..where((t) => t.id.equals(diveId))).getSingle(); + return ComputerTissueSnapshot.tryDecode(row.computerTissueJson); + } + + Future importWith( + ComputerTissueSnapshot? computerTissue, { + bool forceNew = false, + }) { + return repository.importProfile( + computerId: computerId, + profileStartTime: DateTime(2026, 5, 1, 9, 0), + points: points, + durationSeconds: 1800, + maxDepth: 18.0, + decoAlgorithm: 'rgbm', + computerTissue: computerTissue, + forceNew: forceNew, + ); + } + + group('new dive', () { + test('stores the snapshot on the dive row', () async { + final diveId = await importWith(snapshot, forceNew: true); + + expect(await storedTissueFor(diveId), snapshot); + }); + + test('leaves the column null when the computer reported none', () async { + final diveId = await importWith(null, forceNew: true); + + expect(await storedTissueFor(diveId), isNull); + }); + }); + + group('existing dive', () { + test('a re-import with a new snapshot replaces the stored one', () async { + final diveId = await importWith(snapshot, forceNew: true); + await repository.clearSourceAndProfiles( + diveId: diveId, + computerId: computerId, + ); + const updated = ComputerTissueSnapshot( + algorithm: 'Suunto Fused2 RGBM', + end: ComputerTissueState(n2Bar: [1.0, 1.0, 1.0]), + ); + + final reimportedId = await importWith(updated); + + expect(reimportedId, diveId); + expect(await storedTissueFor(diveId), updated); + }); + + test('a re-import without a snapshot keeps the stored one', () async { + final diveId = await importWith(snapshot, forceNew: true); + await repository.clearSourceAndProfiles( + diveId: diveId, + computerId: computerId, + ); + + final reimportedId = await importWith(null); + + expect(reimportedId, diveId); + expect(await storedTissueFor(diveId), snapshot); + }); + }); +} diff --git a/test/features/dive_log/data/repositories/profile_series_repository_test.dart b/test/features/dive_log/data/repositories/profile_series_repository_test.dart index 9bbaccc839..b1d1834a37 100644 --- a/test/features/dive_log/data/repositories/profile_series_repository_test.dart +++ b/test/features/dive_log/data/repositories/profile_series_repository_test.dart @@ -3,6 +3,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:submersion/core/database/database.dart'; import 'package:submersion/features/dive_log/data/repositories/profile_series_repository.dart'; import 'package:submersion/features/dive_log/domain/codecs/profile_sample.dart'; +import 'package:submersion/features/dive_log/domain/codecs/profile_series_codec.dart'; import '../../../../helpers/test_database.dart'; @@ -70,7 +71,7 @@ void main() { expect(row.hasDecoType, isTrue); expect(row.hasDecoStop, isTrue); expect(row.hasPositiveCeiling, isTrue); - expect(row.codecVersion, 1); + expect(row.codecVersion, ProfileSeriesCodec.version); expect(row.createdAt, now); expect(row.updatedAt, now); diff --git a/test/features/dive_log/domain/codecs/profile_sample_test.dart b/test/features/dive_log/domain/codecs/profile_sample_test.dart index 488688f2ff..2e5df6d782 100644 --- a/test/features/dive_log/domain/codecs/profile_sample_test.dart +++ b/test/features/dive_log/domain/codecs/profile_sample_test.dart @@ -31,6 +31,8 @@ void main() { rbt: 1500, decoType: 2, tts: 900, + gf99: 47, + n2Load: 81, ); test('fromPoint then toPoint is the identity on every point field', () { @@ -54,6 +56,8 @@ void main() { expect(sample.temperature, isNull); expect(sample.heartRateSource, isNull); expect(sample.o2SensorMv6, isNull); + expect(sample.gf99, isNull); + expect(sample.n2Load, isNull); expect(sample.toPoint(), minimal); }); @@ -64,6 +68,6 @@ void main() { expect(a, b); expect(a.hashCode, b.hashCode); expect(a, isNot(c)); - expect(a.props, hasLength(28)); + expect(a.props, hasLength(30)); }); } diff --git a/test/features/dive_log/domain/codecs/profile_series_codec_test.dart b/test/features/dive_log/domain/codecs/profile_series_codec_test.dart index 90671a707f..7a0b893e78 100644 --- a/test/features/dive_log/domain/codecs/profile_series_codec_test.dart +++ b/test/features/dive_log/domain/codecs/profile_series_codec_test.dart @@ -12,7 +12,7 @@ import 'package:submersion/features/dive_log/domain/codecs/profile_series_codec. import 'package:submersion/features/dive_log/domain/codecs/profile_series_codec_exception.dart'; import 'package:submersion/features/dive_log/domain/codecs/profile_series_summary.dart'; -/// A fully populated sample: every one of the 28 fields present. +/// A fully populated sample: every one of the 30 fields present. ProfileSample fullSample(int i) => ProfileSample( timestamp: i * 10, depth: 10.0 + i * 0.5, @@ -42,6 +42,8 @@ ProfileSample fullSample(int i) => ProfileSample( o2SensorMv4: 50, o2SensorMv5: 54, o2SensorMv6: 52, + gf99: 30 + i, + n2Load: 60 + i * 2, ); /// Depth and timestamp only, like a manually entered profile. @@ -55,10 +57,53 @@ void main() { test('every field present', () { final samples = [for (var i = 0; i < 12; i++) fullSample(i)]; final encoded = codec.encode(samples); - expect(encoded.codecVersion, 1); + expect(encoded.codecVersion, ProfileSeriesCodec.version); expect(codec.decode(encoded.bytes), samples); }); + test('v2 carries gf99 and n2Load', () { + final samples = [ + for (var i = 0; i < 6; i++) + ProfileSample( + timestamp: i * 10, + depth: 10.0, + gf99: i.isEven ? 40 + i : null, + n2Load: 70 - i, + ), + ]; + final encoded = codec.encode(samples); + expect(encoded.codecVersion, 2); + final decoded = codec.decode(encoded.bytes); + expect(decoded, samples); + expect(decoded[0].gf99, 40); + expect(decoded[1].gf99, isNull); + expect(decoded[5].n2Load, 65); + }); + + test('a v1 blob decodes with gf99 and n2Load null', () { + final samples = [for (var i = 0; i < 6; i++) fullSample(i)]; + final encoded = codec.encode(samples, version: 1); + expect(encoded.codecVersion, 1); + final decoded = codec.decode(encoded.bytes); + for (var i = 0; i < 6; i++) { + expect(decoded[i].gf99, isNull); + expect(decoded[i].n2Load, isNull); + expect(decoded[i], samples[i].withTissue()); + } + }); + + test('v1 bytes are the same whether or not the new fields are set', () { + final bare = [for (var i = 0; i < 6; i++) minimalSample(i)]; + final withTissue = [ + for (final s in bare) s.withTissue(gf99: 50, n2Load: 80), + ]; + expect( + codec.encode(withTissue, version: 1).bytes, + codec.encode(bare, version: 1).bytes, + reason: 'the v1 table has no column for them, so they cannot leak', + ); + }); + test('every optional field null', () { final samples = [for (var i = 0; i < 12; i++) minimalSample(i)]; expect(codec.decode(codec.encode(samples).bytes), samples); @@ -218,7 +263,7 @@ void main() { isA().having( (e) => e.message, 'message', - contains('cns is float64 in v1'), + contains('cns is frozen as float64'), ), ), ); @@ -799,7 +844,8 @@ void main() { ]; final bad = ProfileSeriesCodec(fieldTables: {1: duplicated}); expect(() => bad.encode([fullSample(0)]), throwsArgumentError); - final bytes = codec.encode([fullSample(0)]).bytes; + // Written as v1 so the failure is the duplicate, not the version. + final bytes = codec.encode([fullSample(0)], version: 1).bytes; expect(() => bad.decode(bytes), throwsArgumentError); }); }); diff --git a/test/features/dive_log/domain/codecs/profile_series_field_table_test.dart b/test/features/dive_log/domain/codecs/profile_series_field_table_test.dart index 342a02c0be..0ce034452b 100644 --- a/test/features/dive_log/domain/codecs/profile_series_field_table_test.dart +++ b/test/features/dive_log/domain/codecs/profile_series_field_table_test.dart @@ -41,7 +41,31 @@ const frozenFieldTableV1 = <(String, ProfileFieldKind)>[ ('o2_sensor_mv6', ProfileFieldKind.deltaInt), ]; +/// The v2 field table, frozen the same way: v1 in full, then the two +/// computer-reported tissue fields. +const frozenFieldTableV2 = <(String, ProfileFieldKind)>[ + ...frozenFieldTableV1, + ('gf99', ProfileFieldKind.deltaInt), + ('n2_load', ProfileFieldKind.deltaInt), +]; + void main() { + test('codec v2 is the version new blobs are written with', () { + expect(ProfileSeriesCodec.version, 2); + expect(const ProfileSeriesCodec().fieldTables.keys, {1, 2}); + }); + + test('codec v2 carries exactly the frozen field table, in order', () { + expect( + [for (final f in ProfileSeriesCodec.fieldTableV2) (f.name, f.kind)], + frozenFieldTableV2, + reason: + 'fieldTableV2 changed. Every v2 blob already written decodes ' + 'against this exact order. Append the new field under a NEW ' + 'version in ProfileSeriesCodec; never edit fieldTableV2.', + ); + }); + test('codec v1 carries exactly the frozen field table, in order', () { expect( [for (final f in ProfileSeriesCodec.fieldTableV1) (f.name, f.kind)], @@ -54,12 +78,12 @@ void main() { ); }); - test('the field table covers every ProfileSample field', () { + test('the newest field table covers every ProfileSample field', () { // Anchored on the live type, not a second copy of the list above: a // sample field added without a matching field-table entry would be // silently dropped on encode, and this is what says so. expect( - kProfileFieldTableV1.length, + kProfileFieldTableV2.length, const ProfileSample(timestamp: 0, depth: 0.0).props.length, ); }); diff --git a/test/features/dive_log/domain/codecs/profile_series_golden_test.dart b/test/features/dive_log/domain/codecs/profile_series_golden_test.dart index c5a975c01d..30d36c26ed 100644 --- a/test/features/dive_log/domain/codecs/profile_series_golden_test.dart +++ b/test/features/dive_log/domain/codecs/profile_series_golden_test.dart @@ -153,6 +153,22 @@ void main() { 1, 18, 200, 1, 1, 20, 200, 1, 1, 22, 200, 1, ]; + /// The v1 samples with the two v2 fields set. + final samplesV2 = [ + samples[0].withTissue(gf99: 12, n2Load: 13), + samples[1].withTissue(gf99: 112, n2Load: 113), + ]; + + // v2 is v1 with the version byte 2 and two deltaInt columns appended: + // [1, 24, 200, 1] gf99: presence kPresenceAll, zigzag(12), zigzag(100) + // [1, 26, 200, 1] n2_load: presence kPresenceAll, zigzag(13), zigzag(100) + final goldenBodyV2 = [ + 2, + ...goldenBody.sublist(1), + 1, 24, 200, 1, // + 1, 26, 200, 1, + ]; + const tankSamples = [ TankPressureSample(timestamp: 0, pressure: 200.0), TankPressureSample(timestamp: 10, pressure: 190.5), @@ -174,7 +190,7 @@ void main() { group('profile series codec v1', () { test('encoding freezes to the golden body', () { - final encoded = codec.encode(samples); + final encoded = codec.encode(samples, version: 1); expect(inflate(encoded.bytes), goldenBody); }); @@ -183,6 +199,25 @@ void main() { }); }); + group('profile series codec v2', () { + test('encoding freezes to the golden body', () { + final encoded = codec.encode(samplesV2); + expect(encoded.codecVersion, 2); + expect(inflate(encoded.bytes), goldenBodyV2); + }); + + test('the golden body decodes to the source samples', () { + expect(codec.decode(recompress(goldenBodyV2)), samplesV2); + }); + + test('the v1 golden body still decodes, with the v2 fields null', () { + final decoded = codec.decode(recompress(goldenBody)); + expect(decoded, samples); + expect(decoded.map((s) => s.gf99), everyElement(isNull)); + expect(decoded.map((s) => s.n2Load), everyElement(isNull)); + }); + }); + group('tank pressure series codec v1', () { test('encoding freezes to the golden body', () { final encoded = tankCodec.encode(tankSamples); diff --git a/test/features/dive_log/domain/entities/computer_tissue_snapshot_test.dart b/test/features/dive_log/domain/entities/computer_tissue_snapshot_test.dart new file mode 100644 index 0000000000..4da315af92 --- /dev/null +++ b/test/features/dive_log/domain/entities/computer_tissue_snapshot_test.dart @@ -0,0 +1,216 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:submersion/features/dive_log/domain/entities/computer_tissue_snapshot.dart'; + +void main() { + const full = ComputerTissueSnapshot( + algorithm: 'Suunto Fused2 RGBM', + start: ComputerTissueState( + n2Bar: [0.79, 0.8, 0.81], + heBar: [0.0, 0.0, 0.0], + n2LoadPercent: 12.0, + surfaceGfPercent: 5.0, + ), + end: ComputerTissueState( + n2Bar: [1.5, 1.4, 1.3], + heBar: [0.1, 0.2, 0.3], + loadPercent: [80.0, 75.0, 70.0], + n2LoadPercent: 78.0, + gf99Percent: 42.0, + surfaceGfPercent: 61.0, + cnsPercent: 14.0, + otu: 22.0, + rgbmNitrogen: 0.97, + rgbmHelium: 1.0, + ), + ); + + group('ComputerTissueState', () { + test('compartment count comes from n2Bar first, then loadPercent', () { + const tensions = ComputerTissueState(n2Bar: [1.0, 1.1]); + const loads = ComputerTissueState(loadPercent: [10.0, 20.0, 30.0]); + const scalars = ComputerTissueState(gf99Percent: 40.0); + expect(tensions.compartmentCount, 2); + expect(loads.compartmentCount, 3); + expect(scalars.compartmentCount, isNull); + expect(tensions.hasCompartments, isTrue); + expect(loads.hasCompartments, isTrue); + expect(scalars.hasCompartments, isFalse); + expect(const ComputerTissueState(n2Bar: []).hasCompartments, isFalse); + }); + + test('toJson uses snake_case keys and omits null fields', () { + final json = full.end!.toJson(); + expect(json.keys, { + 'n2_bar', + 'he_bar', + 'load_percent', + 'n2_load_percent', + 'gf99_percent', + 'surface_gf_percent', + 'cns_percent', + 'otu', + 'rgbm_nitrogen', + 'rgbm_helium', + }); + expect(full.start!.toJson().keys, { + 'n2_bar', + 'he_bar', + 'n2_load_percent', + 'surface_gf_percent', + }); + }); + + test('value equality is deep over the compartment lists', () { + const a = ComputerTissueState(n2Bar: [1.0, 2.0], gf99Percent: 10.0); + final b = ComputerTissueState( + n2Bar: [1.0, 2.0].toList(), + gf99Percent: 10.0, + ); + const c = ComputerTissueState(n2Bar: [1.0, 2.5], gf99Percent: 10.0); + expect(a, b); + expect(a.hashCode, b.hashCode); + expect(a, isNot(c)); + }); + + test('copyWith replaces only what is given', () { + final changed = full.end!.copyWith(gf99Percent: 50.0); + expect(changed.gf99Percent, 50.0); + expect(changed.n2Bar, full.end!.n2Bar); + expect(changed.otu, full.end!.otu); + }); + }); + + group('ComputerTissueSnapshot', () { + test('round-trips through toJson and fromJson', () { + expect(ComputerTissueSnapshot.fromJson(full.toJson()), full); + }); + + test('round-trips through encode and decode', () { + final text = full.encode(); + expect(ComputerTissueSnapshot.decode(text), full); + }); + + test('top-level JSON keys are algorithm, start and end', () { + expect(full.toJson().keys, {'algorithm', 'start', 'end'}); + expect(const ComputerTissueSnapshot().toJson(), isEmpty); + }); + + test('hasCompartmentData prefers the end state, then the start', () { + expect(full.hasCompartmentData, isTrue); + const startOnly = ComputerTissueSnapshot( + start: ComputerTissueState(loadPercent: [1.0]), + ); + const endWithoutCompartments = ComputerTissueSnapshot( + start: ComputerTissueState(loadPercent: [1.0]), + end: ComputerTissueState(gf99Percent: 30.0), + ); + const scalarsOnly = ComputerTissueSnapshot( + end: ComputerTissueState(gf99Percent: 30.0), + ); + expect(startOnly.hasCompartmentData, isTrue); + // The end state is what the dive ends on; an end without compartments + // does not fall back to the start's. + expect(endWithoutCompartments.hasCompartmentData, isFalse); + expect(scalarsOnly.hasCompartmentData, isFalse); + expect(const ComputerTissueSnapshot().hasCompartmentData, isFalse); + }); + + test('decode returns null on null and blank text', () { + expect(ComputerTissueSnapshot.decode(null), isNull); + expect(ComputerTissueSnapshot.decode(''), isNull); + expect(ComputerTissueSnapshot.decode(' '), isNull); + }); + + test('decode throws only on non-object JSON', () { + expect( + () => ComputerTissueSnapshot.decode('[1, 2]'), + throwsFormatException, + ); + expect(() => ComputerTissueSnapshot.decode('42'), throwsFormatException); + expect( + () => ComputerTissueSnapshot.decode('not json'), + throwsFormatException, + ); + }); + + test('tryDecode swallows what decode would throw on', () { + expect(ComputerTissueSnapshot.tryDecode('[1, 2]'), isNull); + expect(ComputerTissueSnapshot.tryDecode('not json'), isNull); + expect(ComputerTissueSnapshot.tryDecode(null), isNull); + expect(ComputerTissueSnapshot.tryDecode(full.encode()), full); + }); + + test('fromJson tolerates missing keys', () { + expect( + ComputerTissueSnapshot.fromJson(const {}), + const ComputerTissueSnapshot(), + ); + expect( + ComputerTissueSnapshot.fromJson(const {'algorithm': 'zhl_16c'}), + const ComputerTissueSnapshot(algorithm: 'zhl_16c'), + ); + }); + + test('fromJson nulls malformed values instead of throwing', () { + final parsed = ComputerTissueSnapshot.fromJson(const { + 'algorithm': 7, + 'start': 'junk', + 'end': { + 'n2_bar': [1.0, 'x', 3.0], + 'he_bar': 'not a list', + 'load_percent': [50, 60.5], + 'n2_load_percent': 'seventy', + 'gf99_percent': 42, + 'surface_gf_percent': null, + 'cns_percent': true, + 'otu': [1], + 'rgbm_nitrogen': {'a': 1}, + 'rgbm_helium': 1.0, + 'unknown_key': 'ignored', + }, + }); + expect(parsed.algorithm, isNull); + expect(parsed.start, isNull); + final end = parsed.end!; + // One junk element voids the list: compartment order is positional, + // so a list with a hole is not a shorter list. + expect(end.n2Bar, isNull); + expect(end.heBar, isNull); + expect(end.loadPercent, [50.0, 60.5]); + expect(end.n2LoadPercent, isNull); + expect(end.gf99Percent, 42.0); + expect(end.surfaceGfPercent, isNull); + expect(end.cnsPercent, isNull); + expect(end.otu, isNull); + expect(end.rgbmNitrogen, isNull); + expect(end.rgbmHelium, 1.0); + }); + + test('fromJson accepts integers where doubles are expected', () { + final parsed = ComputerTissueSnapshot.fromJson(const { + 'end': { + 'n2_bar': [1, 2], + 'gf99_percent': 55, + }, + }); + expect(parsed.end!.n2Bar, [1.0, 2.0]); + expect(parsed.end!.gf99Percent, 55.0); + }); + + test('from coerces a snapshot, a JSON map or JSON text', () { + expect(ComputerTissueSnapshot.from(full), same(full)); + expect(ComputerTissueSnapshot.from(full.toJson()), full); + expect(ComputerTissueSnapshot.from(full.encode()), full); + expect(ComputerTissueSnapshot.from(null), isNull); + expect(ComputerTissueSnapshot.from(42), isNull); + expect(ComputerTissueSnapshot.from('[1]'), isNull); + }); + + test('copyWith replaces only what is given', () { + final changed = full.copyWith(algorithm: 'buhlmann'); + expect(changed.algorithm, 'buhlmann'); + expect(changed.start, full.start); + expect(changed.end, full.end); + }); + }); +} diff --git a/test/features/dive_log/domain/entities/dive_profile_point_tissue_fields_test.dart b/test/features/dive_log/domain/entities/dive_profile_point_tissue_fields_test.dart new file mode 100644 index 0000000000..2517e51eb9 --- /dev/null +++ b/test/features/dive_log/domain/entities/dive_profile_point_tissue_fields_test.dart @@ -0,0 +1,49 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:submersion/features/dive_log/domain/entities/computer_tissue_snapshot.dart'; +import 'package:submersion/features/dive_log/domain/entities/dive.dart'; + +void main() { + group('DiveProfilePoint gf99 and n2Load', () { + test('default to null', () { + const point = DiveProfilePoint(timestamp: 0, depth: 10.0); + expect(point.gf99, isNull); + expect(point.n2Load, isNull); + }); + + test('take part in equality and copyWith', () { + const a = DiveProfilePoint( + timestamp: 60, + depth: 20.0, + gf99: 45, + n2Load: 72, + ); + const b = DiveProfilePoint( + timestamp: 60, + depth: 20.0, + gf99: 45, + n2Load: 72, + ); + expect(a, b); + expect(a, isNot(a.copyWith(gf99: 46))); + expect(a.copyWith(n2Load: 80).n2Load, 80); + expect(a.copyWith(n2Load: 80).gf99, 45); + }); + }); + + group('Dive computerTissue', () { + final now = DateTime(2026, 1, 1); + const snapshot = ComputerTissueSnapshot( + algorithm: 'buhlmann', + end: ComputerTissueState(gf99Percent: 33.0), + ); + + test('defaults to null and is carried by copyWith and props', () { + final dive = Dive(id: 'd1', diveNumber: 1, dateTime: now); + expect(dive.computerTissue, isNull); + final withTissue = dive.copyWith(computerTissue: snapshot); + expect(withTissue.computerTissue, snapshot); + expect(withTissue, isNot(dive)); + expect(withTissue.copyWith(id: 'd2').computerTissue, snapshot); + }); + }); +} diff --git a/test/features/dive_log/presentation/providers/computer_gf99_overlay_test.dart b/test/features/dive_log/presentation/providers/computer_gf99_overlay_test.dart new file mode 100644 index 0000000000..0856dc2c4b --- /dev/null +++ b/test/features/dive_log/presentation/providers/computer_gf99_overlay_test.dart @@ -0,0 +1,134 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:submersion/core/constants/profile_metrics.dart'; +import 'package:submersion/features/dive_log/data/services/profile_analysis_service.dart'; +import 'package:submersion/features/dive_log/domain/entities/dive.dart'; +import 'package:submersion/features/dive_log/presentation/providers/computer_gf99_overlay.dart'; +import 'package:submersion/features/dive_log/presentation/providers/profile_analysis_provider.dart'; + +DiveProfilePoint _point(int t, {int? gf99, int? n2Load}) => + DiveProfilePoint(timestamp: t, depth: 20, gf99: gf99, n2Load: n2Load); + +void main() { + group('hasComputerGf99', () { + test('is false for an empty profile', () { + expect(hasComputerGf99(const []), isFalse); + }); + + test('is false when no sample carries gf99', () { + expect(hasComputerGf99([_point(0), _point(10)]), isFalse); + }); + + test('is true when any sample carries gf99, including zero', () { + expect(hasComputerGf99([_point(0), _point(10, gf99: 0)]), isTrue); + }); + }); + + group('buildComputerGf99Curve', () { + test('uses the computer value where reported', () { + final curve = buildComputerGf99Curve( + [_point(0, gf99: 12), _point(10, gf99: 45)], + const [1.0, 2.0], + ); + expect(curve, [12.0, 45.0]); + }); + + test('falls back to the calculated value where a sample has none', () { + final curve = buildComputerGf99Curve( + [_point(0, gf99: 12), _point(10), _point(20, gf99: 70)], + const [1.0, 33.5, 3.0], + ); + expect(curve, [12.0, 33.5, 70.0]); + }); + + test('falls back to zero when the calculated curve is short or null', () { + expect(buildComputerGf99Curve([_point(0), _point(10, gf99: 5)], null), [ + 0.0, + 5.0, + ]); + expect( + buildComputerGf99Curve([_point(0, gf99: 5), _point(10)], const [9.0]), + [5.0, 0.0], + ); + }); + + test('has one entry per profile sample', () { + final profile = List.generate( + 7, + (i) => _point(i, gf99: i.isEven ? i : null), + ); + expect(buildComputerGf99Curve(profile, null).length, 7); + }); + }); + + group('buildComputerN2LoadCurve', () { + test('is null when no sample carries n2Load', () { + expect(buildComputerN2LoadCurve([_point(0), _point(10)]), isNull); + expect(buildComputerN2LoadCurve(const []), isNull); + }); + + test('keeps the computer values verbatim with null gaps', () { + final curve = buildComputerN2LoadCurve([ + _point(0, n2Load: 40), + _point(10), + _point(20, n2Load: 62), + ]); + expect(curve, [40, null, 62]); + }); + }); + + group('overlayComputerDecoData gf99Source', () { + final analysis = ProfileAnalysis.empty().copyWith( + gfCurve: const [10.0, 20.0, 30.0], + ); + + test('computer gf99 present: curve comes from the computer and the ' + 'source info says computer', () { + final profile = [_point(0, gf99: 15), _point(10), _point(20, gf99: 55)]; + final (overlaid, info) = overlayComputerDecoData(analysis, profile); + expect(overlaid.gfCurve, [15.0, 20.0, 55.0]); + expect(info.gf99Actual, MetricDataSource.computer); + }); + + test('computer gf99 absent: the calculated curve is retained and the ' + 'source info says calculated', () { + final profile = [_point(0), _point(10), _point(20)]; + final (overlaid, info) = overlayComputerDecoData(analysis, profile); + expect(overlaid.gfCurve, [10.0, 20.0, 30.0]); + expect(info.gf99Actual, MetricDataSource.calculated); + }); + + test('gf99Source calculated ignores computer gf99', () { + final profile = [_point(0, gf99: 15), _point(10, gf99: 25)]; + final (overlaid, info) = overlayComputerDecoData( + analysis, + profile, + gf99Source: MetricDataSource.calculated, + ); + expect(overlaid.gfCurve, [10.0, 20.0, 30.0]); + expect(info.gf99Actual, MetricDataSource.calculated); + }); + + test('n2LoadCurve is exposed whenever the computer reported it, even ' + 'when nothing else is overlaid', () { + final profile = [ + _point(0, n2Load: 41), + _point(10), + _point(20, n2Load: 63), + ]; + final (overlaid, _) = overlayComputerDecoData( + analysis, + profile, + gf99Source: MetricDataSource.calculated, + ); + expect(overlaid.n2LoadCurve, [41, null, 63]); + }); + + test('n2LoadCurve stays null when the computer did not report it', () { + final (overlaid, _) = overlayComputerDecoData(analysis, [ + _point(0), + _point(10), + ]); + expect(overlaid.n2LoadCurve, isNull); + }); + }); +} diff --git a/test/features/dive_log/presentation/widgets/computer_tissue_section_test.dart b/test/features/dive_log/presentation/widgets/computer_tissue_section_test.dart new file mode 100644 index 0000000000..9c62c3b772 --- /dev/null +++ b/test/features/dive_log/presentation/widgets/computer_tissue_section_test.dart @@ -0,0 +1,164 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:submersion/core/constants/units.dart'; +import 'package:submersion/features/dive_log/domain/entities/computer_tissue_snapshot.dart'; +import 'package:submersion/features/dive_log/presentation/widgets/computer_tissue_section.dart'; +import 'package:submersion/features/settings/presentation/providers/settings_providers.dart'; +import 'package:submersion/l10n/arb/app_localizations.dart'; + +import '../../../../helpers/mock_providers.dart'; + +Widget buildSection({ + ComputerTissueSnapshot? snapshot, + double? calculatedGf99Percent, + double? calculatedSurfaceGfPercent, + double? calculatedCnsPercent, + PressureUnit pressureUnit = PressureUnit.bar, +}) { + return ProviderScope( + overrides: [ + settingsProvider.overrideWith( + (ref) => MockSettingsNotifier(AppSettings(pressureUnit: pressureUnit)), + ), + ], + child: MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: Scaffold( + body: SingleChildScrollView( + child: ComputerTissueSection( + snapshot: snapshot, + calculatedGf99Percent: calculatedGf99Percent, + calculatedSurfaceGfPercent: calculatedSurfaceGfPercent, + calculatedCnsPercent: calculatedCnsPercent, + ), + ), + ), + ), + ); +} + +void main() { + testWidgets('renders nothing without a snapshot', (tester) async { + await tester.pumpWidget(buildSection()); + await tester.pumpAndSettle(); + + expect(find.text('Dive computer'), findsNothing); + expect(find.byType(ComputerTissueLoadBar), findsNothing); + }); + + testWidgets('renders a bar per compartment for loadPercent', (tester) async { + const snapshot = ComputerTissueSnapshot( + algorithm: 'RGBM', + end: ComputerTissueState(loadPercent: [40.0, 65.5, 100.0]), + ); + await tester.pumpWidget(buildSection(snapshot: snapshot)); + await tester.pumpAndSettle(); + + expect(find.text('Dive computer'), findsOneWidget); + expect(find.text('RGBM'), findsOneWidget); + expect(find.text('3 RGBM compartments'), findsOneWidget); + expect(find.byType(ComputerTissueLoadBar), findsNWidgets(3)); + expect(find.text('40%'), findsOneWidget); + expect(find.text('66%'), findsOneWidget); + expect(find.text('100%'), findsOneWidget); + }); + + testWidgets('renders tensions in bar when that is the active unit', ( + tester, + ) async { + const snapshot = ComputerTissueSnapshot( + end: ComputerTissueState(n2Bar: [1.25, 0.9], heBar: [0.1, 0.0]), + ); + await tester.pumpWidget(buildSection(snapshot: snapshot)); + await tester.pumpAndSettle(); + + expect(find.text('2 compartments'), findsOneWidget); + expect(find.text('1.25 bar'), findsOneWidget); + expect(find.text('0.90 bar'), findsOneWidget); + expect(find.text('0.10 bar'), findsOneWidget); + expect(find.text('0.00 bar'), findsOneWidget); + expect(find.byType(ComputerTissueLoadBar), findsNothing); + }); + + testWidgets('renders tensions in psi when that is the active unit', ( + tester, + ) async { + const snapshot = ComputerTissueSnapshot( + end: ComputerTissueState(n2Bar: [1.0]), + ); + await tester.pumpWidget( + buildSection(snapshot: snapshot, pressureUnit: PressureUnit.psi), + ); + await tester.pumpAndSettle(); + + // 1 bar is 14.5 psi. + expect(find.text('14.5 psi'), findsOneWidget); + expect(find.textContaining('bar'), findsNothing); + }); + + testWidgets('renders the start/end aggregate table with calculated lines', ( + tester, + ) async { + const snapshot = ComputerTissueSnapshot( + start: ComputerTissueState(n2LoadPercent: 12, gf99Percent: 0), + end: ComputerTissueState( + n2LoadPercent: 58, + gf99Percent: 45, + surfaceGfPercent: 72, + cnsPercent: 8, + otu: 21.4, + rgbmNitrogen: 0.95, + ), + ); + await tester.pumpWidget( + buildSection( + snapshot: snapshot, + calculatedGf99Percent: 41.2, + calculatedSurfaceGfPercent: 69.7, + calculatedCnsPercent: 7.4, + ), + ); + await tester.pumpAndSettle(); + + expect(find.text('Start'), findsOneWidget); + expect(find.text('End'), findsOneWidget); + expect(find.text('N₂ load'), findsOneWidget); + expect(find.text('12%'), findsOneWidget); + expect(find.text('58%'), findsOneWidget); + expect(find.text('GF99'), findsOneWidget); + expect(find.text('0%'), findsOneWidget); + expect(find.text('45%'), findsOneWidget); + expect(find.text('SurfGF'), findsOneWidget); + expect(find.text('72%'), findsOneWidget); + expect(find.text('CNS'), findsOneWidget); + expect(find.text('8%'), findsOneWidget); + expect(find.text('OTU'), findsOneWidget); + expect(find.text('21'), findsOneWidget); + // Missing start values are shown as a dash, not omitted. + expect(find.text('--'), findsNWidgets(3)); + // Calculated values sit under the computer's as a secondary line. + expect(find.text('calculated 41%'), findsOneWidget); + expect(find.text('calculated 70%'), findsOneWidget); + expect(find.text('calculated 7%'), findsOneWidget); + // RGBM factors, He absent. + expect(find.text('RGBM N₂ factor'), findsOneWidget); + expect(find.text('0.95'), findsOneWidget); + expect(find.text('RGBM He factor'), findsNothing); + }); + + testWidgets('omits the calculated line when there is nothing to compare', ( + tester, + ) async { + const snapshot = ComputerTissueSnapshot( + end: ComputerTissueState(gf99Percent: 45), + ); + await tester.pumpWidget(buildSection(snapshot: snapshot)); + await tester.pumpAndSettle(); + + expect(find.text('45%'), findsOneWidget); + expect(find.textContaining('calculated'), findsNothing); + expect(find.text('Start'), findsOneWidget); + }); +} diff --git a/test/features/equipment/data/repositories/dive_sensor_summary_repository_test.dart b/test/features/equipment/data/repositories/dive_sensor_summary_repository_test.dart index 30be45c09c..ea4621f327 100644 --- a/test/features/equipment/data/repositories/dive_sensor_summary_repository_test.dart +++ b/test/features/equipment/data/repositories/dive_sensor_summary_repository_test.dart @@ -203,16 +203,16 @@ void main() { maxDepth: 9.0, firstDepth: 1.0, lastDepth: 9.0, - codecVersion: 2, + codecVersion: 3, createdAt: 1000, updatedAt: 1000, samples: const ProfileSeriesCodec( - fieldTables: {2: kProfileFieldTableV1}, + fieldTables: {3: kProfileFieldTableV1}, ).encode(const [ ProfileSample(timestamp: 0, depth: 1.0), ProfileSample(timestamp: 60, depth: 9.0), - ], version: 2).bytes, + ], version: 3).bytes, ).copyWith(isPrimary: const Value(true)), ); diff --git a/test/features/equipment/data/services/sensor_summary_worker_test.dart b/test/features/equipment/data/services/sensor_summary_worker_test.dart index 109946cf10..697ecfdbf3 100644 --- a/test/features/equipment/data/services/sensor_summary_worker_test.dart +++ b/test/features/equipment/data/services/sensor_summary_worker_test.dart @@ -112,15 +112,15 @@ void main() { }); group('forward codec versions', () { - /// A blob written by a hypothetical v2 codec: this build has no field + /// A blob written by a hypothetical v3 codec: this build has no field /// table for it, but the samples are fine and a newer build reads them. Uint8List futureProfile() => - const ProfileSeriesCodec(fieldTables: {2: kProfileFieldTableV1}).encode( + const ProfileSeriesCodec(fieldTables: {3: kProfileFieldTableV1}).encode( const [ ProfileSample(timestamp: 0, depth: 1.0), ProfileSample(timestamp: 60, depth: 9.0), ], - version: 2, + version: 3, ).bytes; test('a forward-version profile blob is refused, not summarised', () { diff --git a/test/features/import_wizard/data/adapters/suunto_cloud_adapter_test.dart b/test/features/import_wizard/data/adapters/suunto_cloud_adapter_test.dart index 05e37d51a7..53efea6856 100644 --- a/test/features/import_wizard/data/adapters/suunto_cloud_adapter_test.dart +++ b/test/features/import_wizard/data/adapters/suunto_cloud_adapter_test.dart @@ -11,6 +11,7 @@ import 'package:submersion/features/dive_log/data/repositories/dive_computer_rep import 'package:submersion/features/dive_log/data/repositories/dive_repository_impl.dart'; import 'package:submersion/features/dive_log/data/services/dive_consolidation_service.dart'; import 'package:submersion/features/dive_log/domain/services/unreadable_series_exception.dart'; +import 'package:submersion/features/dive_log/domain/entities/computer_tissue_snapshot.dart'; import 'package:submersion/features/dive_log/domain/entities/dive_computer.dart'; import 'package:submersion/features/import_wizard/data/adapters/suunto_cloud_adapter.dart'; import 'package:submersion/features/import_wizard/domain/models/duplicate_action.dart'; @@ -32,6 +33,7 @@ SuuntoParsedDive makeParsedDive({ String? deviceName = 'Suunto Ocean', String? serialNumber = 'SN-1', String? firmwareVersion, + ComputerTissueSnapshot? computerTissue, }) { return SuuntoParsedDive( dive: DownloadedDive( @@ -39,6 +41,7 @@ SuuntoParsedDive makeParsedDive({ durationSeconds: durationSeconds, maxDepth: maxDepth, profile: const [], + computerTissue: computerTissue, ), deviceName: deviceName, serialNumber: serialNumber, @@ -264,6 +267,39 @@ void main() { }); }); + group('computer tissue', () { + test( + 'the snapshot rides on the dive handed to the import service', + () async { + const snapshot = ComputerTissueSnapshot( + algorithm: 'Suunto Fused2 RGBM', + start: ComputerTissueState(n2Bar: [0.79, 0.79]), + end: ComputerTissueState(n2Bar: [0.9, 1.1], cnsPercent: 13.2), + ); + adapter.setParsedDives([makeParsedDive(computerTissue: snapshot)]); + final bundle = await adapter.buildBundle(); + stubImportAsNew(); + + await adapter.performImport(bundle, { + ImportEntityType.dives: {0}, + }, {}); + + final imported = + verify( + mockImportService.importSingleDiveAsNew( + captureAny, + computerId: anyNamed('computerId'), + diverId: anyNamed('diverId'), + descriptorVendor: anyNamed('descriptorVendor'), + descriptorProduct: anyNamed('descriptorProduct'), + ), + ).captured.single + as DownloadedDive; + expect(imported.computerTissue, snapshot); + }, + ); + }); + group('retaining source dive numbers (issue #1832)', () { test('forwards the option to the import service', () async { adapter.setParsedDives([makeParsedDive()]); diff --git a/test/features/universal_import/data/parsers/fit_import_parser_test.dart b/test/features/universal_import/data/parsers/fit_import_parser_test.dart index a1be350a29..88e7518ebf 100644 --- a/test/features/universal_import/data/parsers/fit_import_parser_test.dart +++ b/test/features/universal_import/data/parsers/fit_import_parser_test.dart @@ -196,6 +196,15 @@ void main() { }, ); + test('omits n2Load and computerTissue when the file records no N2', () async { + final payload = await const FitImportParser().parse(_richFitBytes()); + final d = payload.entities[ImportEntityType.dives]!.single; + + expect(d.containsKey('computerTissue'), isFalse); + final profile = d['profile'] as List>; + expect(profile.any((p) => p.containsKey('n2Load')), isFalse); + }); + test( 'emits tank pressure, allTankPressures, exit GPS, and heart rate', () async { diff --git a/test/features/universal_import/data/parsers/fit_import_parser_tissue_test.dart b/test/features/universal_import/data/parsers/fit_import_parser_tissue_test.dart new file mode 100644 index 0000000000..060d96eca3 --- /dev/null +++ b/test/features/universal_import/data/parsers/fit_import_parser_tissue_test.dart @@ -0,0 +1,45 @@ +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:submersion/features/dive_log/domain/entities/computer_tissue_snapshot.dart'; +import 'package:submersion/features/universal_import/data/models/import_enums.dart'; +import 'package:submersion/features/universal_import/data/parsers/fit_import_parser.dart'; + +const _fixture = 'test/dives/005_oc-trimix-two-deco-gases.fit'; + +void main() { + group('FitImportParser computer tissue (real Descent file)', () { + late Map dive; + + setUpAll(() async { + final bytes = Uint8List.fromList(File(_fixture).readAsBytesSync()); + final payload = await const FitImportParser().parse(bytes); + dive = payload.entities[ImportEntityType.dives]!.single; + }); + + test( + 'carries the dive_summary N2 loading as a computerTissue snapshot', + () { + final tissue = dive['computerTissue']; + expect(tissue, isA()); + final snapshot = tissue as ComputerTissueSnapshot; + expect(snapshot.algorithm, 'zhl_16c'); + expect(snapshot.start?.n2LoadPercent, 0); + expect(snapshot.end?.n2LoadPercent, 86); + }, + ); + + test('carries the per-record N2 loading on the profile points', () { + final profile = dive['profile'] as List>; + final loads = profile.map((p) => p['n2Load']).whereType().toList(); + + // Only the very first record lacks n2_load; the rest ride along. + expect(profile.first.containsKey('n2Load'), isFalse); + expect(loads, hasLength(profile.length - 1)); + expect(loads.first, 2); + expect(loads.last, 86); + expect(loads.reduce((a, b) => a > b ? a : b), 191); + }); + }); +} diff --git a/test/features/universal_import/data/parsers/ratio_xml_parser_tissue_test.dart b/test/features/universal_import/data/parsers/ratio_xml_parser_tissue_test.dart new file mode 100644 index 0000000000..0ed06acd18 --- /dev/null +++ b/test/features/universal_import/data/parsers/ratio_xml_parser_tissue_test.dart @@ -0,0 +1,203 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:submersion/features/dive_log/domain/entities/computer_tissue_snapshot.dart'; +import 'package:submersion/features/universal_import/data/models/import_enums.dart'; +import 'package:submersion/features/universal_import/data/models/import_options.dart'; +import 'package:submersion/features/universal_import/data/parsers/ratio_xml_parser.dart'; + +const _fixturePath = + 'test/fixtures/universal_import/ratio_xml/' + 'IX3M_2_PRO_123456-dive_14-19880731_190154.xml'; + +/// Builds a minimal Ratio XML document whose samples carry the given +/// extra elements (values are written verbatim, so text is allowed). +String _ratioXml(List> samples) { + final buf = StringBuffer() + ..writeln('') + ..writeln('') + ..writeln('') + ..writeln('586371714') + ..writeln('1200') + ..writeln('') + ..writeln(''); + for (final sample in samples) { + buf.writeln(''); + sample.forEach((k, v) => buf.writeln('<$k>$v')); + buf.writeln(''); + } + buf + ..writeln('') + ..writeln(''); + return buf.toString(); +} + +Map _sample( + int runtimeS, { + Map extra = const {}, +}) { + return { + 'runtimeS': runtimeS, + 'depthDm': 100, + 'activeMixO2Percent': 21, + 'activeAlgorithm': 0, + 'CNS': 1, + ...extra, + }; +} + +Map _groups(List values) => { + for (var i = 0; i < values.length; i++) + 'tissueGroup${i + 1}Percent': values[i], +}; + +Future> _parseDive(String xml) async { + const parser = RatioXmlParser(); + final payload = await parser.parse( + Uint8List.fromList(utf8.encode(xml)), + options: const ImportOptions( + sourceApp: SourceApp.ratio, + format: ImportFormat.ratioXml, + ), + ); + return payload.entities[ImportEntityType.dives]!.single; +} + +void main() { + group('RatioXmlParser computer tissue', () { + test( + 'reads start and end compartment loading from the iX3M fixture', + () async { + final bytes = await File(_fixturePath).readAsBytes(); + const parser = RatioXmlParser(); + final payload = await parser.parse( + bytes, + options: const ImportOptions( + sourceApp: SourceApp.ratio, + format: ImportFormat.ratioXml, + fileName: 'IX3M_2_PRO_123456-dive_14-19880731_190154.xml', + ), + ); + final dive = payload.entities[ImportEntityType.dives]!.single; + + final tissue = dive['computerTissue']; + expect(tissue, isA()); + final snapshot = tissue as ComputerTissueSnapshot; + + expect(snapshot.algorithm, 'Buhlmann'); + + // First sample of the fixture. + expect(snapshot.start?.loadPercent, hasLength(16)); + expect(snapshot.start?.loadPercent?.first, 7.0); + expect(snapshot.start?.loadPercent, [ + 7.0, 7.0, 7.0, 8.0, 8.0, 8.0, 8.0, 8.0, // + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + ]); + expect(snapshot.start?.cnsPercent, 1.0); + + // Last sample of the fixture. + expect(snapshot.end?.loadPercent, hasLength(16)); + expect(snapshot.end?.loadPercent, [ + 10.0, 12.0, 12.0, 12.0, 11.0, 11.0, 10.0, 10.0, // + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + ]); + expect(snapshot.end?.cnsPercent, 1.0); + expect(snapshot.hasCompartmentData, isTrue); + }, + ); + + test('sets no computerTissue when no sample has tissue groups', () async { + final dive = await _parseDive(_ratioXml([_sample(10), _sample(20)])); + + expect(dive.containsKey('computerTissue'), isFalse); + }); + + test('names VPM when the active algorithm is 1', () async { + final dive = await _parseDive( + _ratioXml([ + _sample( + 10, + extra: { + 'activeAlgorithm': 1, + ..._groups([5, 6]), + }, + ), + ]), + ); + + final snapshot = dive['computerTissue'] as ComputerTissueSnapshot; + expect(snapshot.algorithm, 'VPM'); + expect(snapshot.end?.loadPercent, [5.0, 6.0]); + }); + + test('stops the compartment list at the first missing index', () async { + final dive = await _parseDive( + _ratioXml([ + _sample( + 10, + extra: { + 'tissueGroup1Percent': 3, + 'tissueGroup2Percent': 4, + 'tissueGroup4Percent': 9, + }, + ), + ]), + ); + + final snapshot = dive['computerTissue'] as ComputerTissueSnapshot; + expect(snapshot.end?.loadPercent, [3.0, 4.0]); + }); + + test('treats a non-numeric group as missing and stops there', () async { + final dive = await _parseDive( + _ratioXml([ + _sample(10, extra: _groups([2, 'n/a', 7])), + ]), + ); + + final snapshot = dive['computerTissue'] as ComputerTissueSnapshot; + expect(snapshot.end?.loadPercent, [2.0]); + }); + + test('ignores a sample whose first group is non-numeric', () async { + final dive = await _parseDive( + _ratioXml([ + _sample(10, extra: _groups(['x', 4])), + _sample(20), + ]), + ); + + expect(dive.containsKey('computerTissue'), isFalse); + }); + + test('takes start from the first and end from the last sample', () async { + final dive = await _parseDive( + _ratioXml([ + _sample( + 10, + extra: { + ..._groups([1, 2]), + 'CNS': 0, + }, + ), + _sample(20, extra: _groups([3, 4])), + _sample( + 30, + extra: { + ..._groups([5, 6]), + 'CNS': 4, + }, + ), + ]), + ); + + final snapshot = dive['computerTissue'] as ComputerTissueSnapshot; + expect(snapshot.start?.loadPercent, [1.0, 2.0]); + expect(snapshot.start?.cnsPercent, 0.0); + expect(snapshot.end?.loadPercent, [5.0, 6.0]); + expect(snapshot.end?.cnsPercent, 4.0); + }); + }); +} diff --git a/test/features/universal_import/data/parsers/uddf_import_parser_gf99_test.dart b/test/features/universal_import/data/parsers/uddf_import_parser_gf99_test.dart new file mode 100644 index 0000000000..63b11b3aa3 --- /dev/null +++ b/test/features/universal_import/data/parsers/uddf_import_parser_gf99_test.dart @@ -0,0 +1,27 @@ +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:submersion/features/universal_import/data/models/import_enums.dart'; +import 'package:submersion/features/universal_import/data/parsers/uddf_import_parser.dart'; + +/// The universal import wizard reads UDDF through [UddfImportParser]; the +/// per-waypoint GF99 has to survive that path to reach the entity importer. +const _petrel3 = 'test/dives/006_ccr_petrel3_shearwater-cloud-export.uddf'; + +void main() { + test('carries the waypoint GF99 as gf99 on the dive samples', () async { + final payload = await UddfImportParser().parse( + Uint8List.fromList(File(_petrel3).readAsBytesSync()), + ); + + final dives = payload.entities[ImportEntityType.dives]!; + expect(dives, hasLength(1)); + final profile = dives.single['profile'] as List>; + final values = profile.map((p) => p['gf99']).whereType().toList(); + + expect(values.length, greaterThan(100)); + expect(values.every((v) => v >= 0 && v <= 200), isTrue); + expect(values.last, 59); + }); +} diff --git a/test/features/universal_import/data/services/shearwater_db_reader_test.dart b/test/features/universal_import/data/services/shearwater_db_reader_test.dart index 41bd171f2a..f83256ee9e 100644 --- a/test/features/universal_import/data/services/shearwater_db_reader_test.dart +++ b/test/features/universal_import/data/services/shearwater_db_reader_test.dart @@ -391,6 +391,213 @@ void main() { }); }); + group('computer tissue columns', () { + const records = [ + ShearwaterTestLogRecord(currentTime: 0, currentDepth: 0, gf99: 5), + ShearwaterTestLogRecord(currentTime: 10000, currentDepth: 8, gf99: 12), + ShearwaterTestLogRecord( + currentTime: 20000, + currentDepth: 15, + gf99: null, + ), + ShearwaterTestLogRecord(currentTime: 30000, currentDepth: 20, gf99: 40), + ]; + + test('leaves the series empty and header values null when the ' + 'optional tables are absent (older export)', () async { + final bytes = createShearwaterTestDb( + dives: [const ShearwaterTestDive(diveId: 'old-export')], + ); + + expect(await ShearwaterDbReader.isShearwaterCloudDb(bytes), isTrue); + final dives = await ShearwaterDbReader.readDives(bytes); + final dive = dives.single; + expect(dive.gf99Samples, isEmpty); + expect(dive.startGFS, isNull); + expect(dive.gfMin, isNull); + expect(dive.gfMax, isNull); + expect(dive.decoModel, isNull); + expect(dive.startCNS, isNull); + expect(dive.endCNS, isNull); + }); + + test('reads the gf99 series joined on diveLogId, ordered by time, ' + 'with millisecond currentTime converted to seconds', () async { + final bytes = createShearwaterTestDb( + includeDiveLogRecords: true, + dives: [ + ShearwaterTestDive( + diveId: 'gf-dive', + // Deliberately out of order: the reader must sort by time. + logRecords: [records[3], records[0], records[2], records[1]], + ), + const ShearwaterTestDive( + diveId: 'other-dive', + logRecords: [ShearwaterTestLogRecord(currentTime: 0, gf99: 99)], + ), + ], + ); + + final dives = await ShearwaterDbReader.readDives(bytes); + final dive = dives.firstWhere((d) => d.diveId == 'gf-dive'); + expect(dive.gf99Samples.map((s) => s.timeSeconds), [0, 10, 30]); + expect(dive.gf99Samples.map((s) => s.gf99), [5, 12, 40]); + + final other = dives.firstWhere((d) => d.diveId == 'other-dive'); + expect(other.gf99Samples.map((s) => s.gf99), [99]); + }); + + test( + 'keeps currentTime as seconds when rows are seconds apart', + () async { + final bytes = createShearwaterTestDb( + includeDiveLogRecords: true, + dives: [ + const ShearwaterTestDive( + diveId: 'seconds', + logRecords: [ + ShearwaterTestLogRecord(currentTime: 0, gf99: 1), + ShearwaterTestLogRecord(currentTime: 10, gf99: 2), + ShearwaterTestLogRecord(currentTime: 20, gf99: 3), + ], + ), + ], + ); + + final dives = await ShearwaterDbReader.readDives(bytes); + expect(dives.single.gf99Samples.map((s) => s.timeSeconds), [ + 0, + 10, + 20, + ]); + }, + ); + + test( + 'returns an empty series when dive_log_records lacks gf99', + () async { + final bytes = createShearwaterTestDb( + includeDiveLogRecords: true, + includeGf99Column: false, + dives: [ + const ShearwaterTestDive( + diveId: 'no-gf-column', + logRecords: [ShearwaterTestLogRecord(currentTime: 0)], + ), + ], + ); + + final dives = await ShearwaterDbReader.readDives(bytes); + expect(dives.single.gf99Samples, isEmpty); + }, + ); + + test('returns an empty series when the dive has no records', () async { + final bytes = createShearwaterTestDb( + includeDiveLogRecords: true, + dives: [const ShearwaterTestDive(diveId: 'no-records')], + ); + + final dives = await ShearwaterDbReader.readDives(bytes); + expect(dives.single.gf99Samples, isEmpty); + }); + + test('reads header values from dive_logs', () async { + final bytes = createShearwaterTestDb( + includeDiveLogs: true, + dives: [ + const ShearwaterTestDive( + diveId: 'header', + diveLog: ShearwaterTestDiveLog( + gfMin: 30, + gfMax: 70, + startCNS: 4, + endCNS: 21, + decoModel: 0, + startGFS: 18, + ), + ), + ], + ); + + final dive = (await ShearwaterDbReader.readDives(bytes)).single; + expect(dive.gfMin, 30); + expect(dive.gfMax, 70); + expect(dive.startCNS, 4.0); + expect(dive.endCNS, 21.0); + expect(dive.startGFS, 18.0); + expect(dive.decoModel, 'GF'); + }); + + test('spells the integer deco model codes as Shearwater does', () async { + Future modelFor(Object? code) async { + final bytes = createShearwaterTestDb( + includeDiveLogs: true, + dives: [ + ShearwaterTestDive( + diveId: 'model', + diveLog: ShearwaterTestDiveLog(decoModel: code), + ), + ], + ); + return (await ShearwaterDbReader.readDives(bytes)).single.decoModel; + } + + expect(await modelFor(0), 'GF'); + expect(await modelFor(1), 'VPM-B'); + expect(await modelFor(2), 'VPM-B/GFS'); + expect(await modelFor(3), 'DCIEM'); + expect(await modelFor('VPM-B/GFS'), 'VPM-B/GFS'); + expect(await modelFor(''), isNull); + expect(await modelFor(null), isNull); + expect(await modelFor(42), isNull); + }); + + test( + 'leaves header values null when the dive has no dive_logs row', + () async { + final bytes = createShearwaterTestDb( + includeDiveLogs: true, + dives: [const ShearwaterTestDive(diveId: 'no-header')], + ); + + final dive = (await ShearwaterDbReader.readDives(bytes)).single; + expect(dive.startGFS, isNull); + expect(dive.decoModel, isNull); + }, + ); + }); + + group('committed fixtures', () { + Uint8List fixture(String name) => + File('test/dives/$name').readAsBytesSync(); + + test('CCR export: tissue tables exist but are empty, EndGF99 lives in ' + 'the calculated values', () async { + final dives = await ShearwaterDbReader.readDives( + fixture('100_shearwater_cloud_export_with_one_ccr_dive.db.export'), + ); + final dive = dives.single; + expect(dive.gf99Samples, isEmpty); + expect(dive.decoModel, isNull); + expect(dive.startGFS, isNull); + // dive_details.EndGF99 is a 0.0 placeholder in this export ... + expect(dive.endGF99, 0.0); + // ... while the value derived from the samples is the real one. + expect(dive.calculatedValues?['EndGF99'], 62.0); + }); + + test('cell demo export: no tissue tables at all still reads', () async { + final dives = await ShearwaterDbReader.readDives( + fixture('102_o2_cell_traffic_light_demo.db.export'), + ); + final dive = dives.single; + expect(dive.gf99Samples, isEmpty); + expect(dive.endGF99, isNull); + expect(dive.calculatedValues, isNull); + }); + }); + group('real fixture', () { late Uint8List dbBytes; diff --git a/test/features/universal_import/data/services/shearwater_dive_mapper_tissue_test.dart b/test/features/universal_import/data/services/shearwater_dive_mapper_tissue_test.dart new file mode 100644 index 0000000000..1cb8f367bb --- /dev/null +++ b/test/features/universal_import/data/services/shearwater_dive_mapper_tissue_test.dart @@ -0,0 +1,186 @@ +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:libdivecomputer_plugin/libdivecomputer_plugin.dart' as pigeon; +import 'package:submersion/features/dive_log/domain/entities/computer_tissue_snapshot.dart'; +import 'package:submersion/features/universal_import/data/services/shearwater_db_reader.dart'; +import 'package:submersion/features/universal_import/data/services/shearwater_dive_mapper.dart'; + +void main() { + ComputerTissueSnapshot? tissueOf(Map dive) => + ComputerTissueSnapshot.from(dive['computerTissue']); + + pigeon.ParsedDive parsedWithSamples(List times) => pigeon.ParsedDive( + fingerprint: '', + dateTimeYear: 2026, + dateTimeMonth: 8, + dateTimeDay: 1, + dateTimeHour: 9, + dateTimeMinute: 8, + dateTimeSecond: 48, + maxDepthMeters: 40, + avgDepthMeters: 25, + durationSeconds: times.isEmpty ? 0 : times.last, + samples: [ + for (final t in times) + pigeon.ProfileSample(timeSeconds: t, depthMeters: 20.0), + ], + tanks: [], + gasMixes: [], + events: [], + ); + + group('ShearwaterDiveMapper computer tissue', () { + test('builds the snapshot from the dive_logs header and EndGF99', () { + const rawDive = ShearwaterRawDive( + diveId: 'tissue', + endGF99: 58, + decoModel: 'VPM-B/GFS', + startGFS: 18, + startCNS: 4, + endCNS: 21, + ); + + final tissue = tissueOf(ShearwaterDiveMapper.mapDiveMetadata(rawDive)); + expect(tissue, isNotNull); + expect(tissue!.algorithm, 'VPM-B/GFS'); + expect(tissue.start?.surfaceGfPercent, 18.0); + expect(tissue.start?.cnsPercent, 4.0); + expect(tissue.end?.gf99Percent, 58.0); + expect(tissue.end?.cnsPercent, 21.0); + }); + + test('prefers the EndGF99 derived from the samples over a 0.0 ' + 'dive_details placeholder', () { + const rawDive = ShearwaterRawDive( + diveId: 'placeholder', + endGF99: 0.0, + calculatedValues: {'EndGF99': 62.0}, + ); + + final tissue = tissueOf(ShearwaterDiveMapper.mapDiveMetadata(rawDive)); + expect(tissue?.end?.gf99Percent, 62.0); + }); + + test('falls back to dive_details.EndGF99 when no calculated value', () { + const rawDive = ShearwaterRawDive(diveId: 'details-only', endGF99: 41); + + final tissue = tissueOf(ShearwaterDiveMapper.mapDiveMetadata(rawDive)); + expect(tissue?.end?.gf99Percent, 41.0); + expect(tissue?.start, isNull); + expect(tissue?.algorithm, isNull); + }); + + test('falls back to the last gf99 sample when neither summary is set', () { + const rawDive = ShearwaterRawDive( + diveId: 'series-only', + endGF99: 0.0, + gf99Samples: [ + ShearwaterGf99Sample(timeSeconds: 0, gf99: 3), + ShearwaterGf99Sample(timeSeconds: 10, gf99: 55), + ], + ); + + final tissue = tissueOf(ShearwaterDiveMapper.mapDiveMetadata(rawDive)); + expect(tissue?.end?.gf99Percent, 55.0); + }); + + test('omits the snapshot when no tissue value is present', () { + const rawDive = ShearwaterRawDive(diveId: 'none', endGF99: 0.0); + + final dive = ShearwaterDiveMapper.mapDiveMetadata(rawDive); + expect(dive.containsKey('computerTissue'), isFalse); + }); + + test('omits the end state when only start values are present', () { + const rawDive = ShearwaterRawDive(diveId: 'start-only', startGFS: 12); + + final tissue = tissueOf(ShearwaterDiveMapper.mapDiveMetadata(rawDive)); + expect(tissue?.start?.surfaceGfPercent, 12.0); + expect(tissue?.end, isNull); + }); + + test('carries gfMin/gfMax as gradient factors in the metadata map', () { + const rawDive = ShearwaterRawDive(diveId: 'gf', gfMin: 30, gfMax: 70); + + final dive = ShearwaterDiveMapper.mapDiveMetadata(rawDive); + expect(dive['gradientFactorLow'], 30); + expect(dive['gradientFactorHigh'], 70); + }); + + test('leaves the gradient factor keys out when dive_logs has none', () { + const rawDive = ShearwaterRawDive(diveId: 'no-gf'); + + final dive = ShearwaterDiveMapper.mapDiveMetadata(rawDive); + expect(dive.containsKey('gradientFactorLow'), isFalse); + expect(dive.containsKey('gradientFactorHigh'), isFalse); + }); + }); + + group('ShearwaterDiveMapper.mergeWithParsedDive gf99 alignment', () { + test('sets gf99 on the libdivecomputer samples by nearest time', () { + const series = [ + ShearwaterGf99Sample(timeSeconds: 0, gf99: 2), + ShearwaterGf99Sample(timeSeconds: 11, gf99: 20), + ShearwaterGf99Sample(timeSeconds: 30, gf99: 62), + ]; + final merged = ShearwaterDiveMapper.mergeWithParsedDive( + {'profile': const >[]}, + parsedWithSamples([0, 10, 20, 30]), + gf99Samples: series, + ); + + final profile = merged['profile'] as List>; + expect(profile.map((p) => p['gf99']), [2, 20, null, 62]); + expect(profile[2].containsKey('gf99'), isFalse); + }); + + test('leaves samples without gf99 when the series is empty', () { + final merged = ShearwaterDiveMapper.mergeWithParsedDive({ + 'profile': const >[], + }, parsedWithSamples([0, 10])); + + final profile = merged['profile'] as List>; + expect(profile.any((p) => p.containsKey('gf99')), isFalse); + }); + + test('the last aligned gf99 agrees with the end snapshot', () { + const rawDive = ShearwaterRawDive( + diveId: 'agree', + calculatedValues: {'EndGF99': 62.0}, + gf99Samples: [ + ShearwaterGf99Sample(timeSeconds: 0, gf99: 5), + ShearwaterGf99Sample(timeSeconds: 10, gf99: 30), + ShearwaterGf99Sample(timeSeconds: 20, gf99: 62), + ], + ); + final merged = ShearwaterDiveMapper.mergeWithParsedDive( + ShearwaterDiveMapper.mapDiveMetadata(rawDive), + parsedWithSamples([0, 10, 20]), + gf99Samples: rawDive.gf99Samples, + ); + + final profile = merged['profile'] as List>; + final lastGf99 = profile.reversed + .map((p) => p['gf99'] as int?) + .firstWhere((v) => v != null); + expect(lastGf99, 62); + expect(tissueOf(merged)?.end?.gf99Percent, closeTo(lastGf99!, 1.0)); + }); + }); + + group('committed CCR fixture', () { + test('maps EndGF99 62 into computerTissue.end', () async { + final bytes = File( + 'test/dives/100_shearwater_cloud_export_with_one_ccr_dive.db.export', + ).readAsBytesSync(); + final rawDive = (await ShearwaterDbReader.readDives(bytes)).single; + + final tissue = tissueOf(ShearwaterDiveMapper.mapDiveMetadata(rawDive)); + expect(tissue?.end?.gf99Percent, 62.0); + // The export carries no dive_logs row, so there is no start state. + expect(tissue?.start, isNull); + expect(tissue?.algorithm, isNull); + }); + }); +} diff --git a/test/features/universal_import/data/services/shearwater_gf99_aligner_test.dart b/test/features/universal_import/data/services/shearwater_gf99_aligner_test.dart new file mode 100644 index 0000000000..36a3bfcd47 --- /dev/null +++ b/test/features/universal_import/data/services/shearwater_gf99_aligner_test.dart @@ -0,0 +1,123 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:submersion/features/universal_import/data/services/shearwater_db_reader.dart'; +import 'package:submersion/features/universal_import/data/services/shearwater_gf99_aligner.dart'; + +void main() { + List> samplesAt(List times) => [ + for (final t in times) {'timestamp': t, 'depth': 10.0}, + ]; + + ShearwaterGf99Sample gf(int t, int value) => + ShearwaterGf99Sample(timeSeconds: t, gf99: value); + + group('ShearwaterGf99Aligner.apply', () { + test('returns the samples untouched when the series is empty', () { + final samples = samplesAt([0, 10, 20]); + final result = ShearwaterGf99Aligner.apply(samples, const []); + expect(result, samples); + expect(result.any((s) => s.containsKey('gf99')), isFalse); + }); + + test('returns an empty list for no samples', () { + final result = ShearwaterGf99Aligner.apply(const [], [gf(0, 5)]); + expect(result, isEmpty); + }); + + test('sets gf99 on samples whose time matches exactly', () { + final result = ShearwaterGf99Aligner.apply(samplesAt([0, 10, 20]), [ + gf(0, 3), + gf(10, 7), + gf(20, 11), + ]); + expect(result.map((s) => s['gf99']), [3, 7, 11]); + }); + + test('picks the nearest point within half the sample interval', () { + // 10 s samples: tolerance is 5 s. + final result = ShearwaterGf99Aligner.apply(samplesAt([0, 10, 20, 30]), [ + gf(4, 1), // 4 s from sample 0: kept + gf(16, 2), // 4 s from sample 20: kept + gf(37, 3), // 7 s from sample 30: too far + ]); + expect(result[0]['gf99'], 1); + expect(result[1].containsKey('gf99'), isFalse); + expect(result[2]['gf99'], 2); + expect(result[3].containsKey('gf99'), isFalse); + }); + + test('prefers the closer of two candidates on either side', () { + final result = ShearwaterGf99Aligner.apply(samplesAt([0, 10, 20]), [ + gf(7, 1), // 3 s before 10 + gf(11, 2), // 1 s after 10: closer + ]); + expect(result[1]['gf99'], 2); + expect(result[0].containsKey('gf99'), isFalse); + expect(result[2].containsKey('gf99'), isFalse); + }); + + test('tolerates an unsorted series', () { + final result = ShearwaterGf99Aligner.apply(samplesAt([0, 10, 20]), [ + gf(20, 3), + gf(0, 1), + gf(10, 2), + ]); + expect(result.map((s) => s['gf99']), [1, 2, 3]); + }); + + test('uses the series spacing for the tolerance when there is a single ' + 'sample', () { + final result = ShearwaterGf99Aligner.apply(samplesAt([12]), [ + gf(0, 1), + gf(10, 2), + gf(20, 3), + ]); + expect(result.single['gf99'], 2); + }); + + test('requires an exact match when no interval can be derived', () { + expect( + ShearwaterGf99Aligner.apply(samplesAt([12]), [gf(10, 2)]).single, + isNot(contains('gf99')), + ); + expect( + ShearwaterGf99Aligner.apply(samplesAt([10]), [ + gf(10, 2), + ]).single['gf99'], + 2, + ); + }); + + test('does not mutate the input maps', () { + final samples = samplesAt([0, 10]); + final before = samples.map(Map.from).toList(); + ShearwaterGf99Aligner.apply(samples, [gf(0, 1), gf(10, 2)]); + expect(samples, before); + }); + + test('keeps every other key of the sample', () { + final samples = [ + {'timestamp': 0, 'depth': 1.0, 'ppO2': 1.2}, + ]; + final result = ShearwaterGf99Aligner.apply(samples, [gf(0, 9)]); + expect(result.single, { + 'timestamp': 0, + 'depth': 1.0, + 'ppO2': 1.2, + 'gf99': 9, + }); + }); + + test('skips samples without a numeric timestamp', () { + final samples = [ + {'depth': 1.0}, + {'timestamp': 10, 'depth': 2.0}, + ]; + final result = ShearwaterGf99Aligner.apply(samples, [ + gf(0, 1), + gf(10, 2), + ]); + expect(result[0].containsKey('gf99'), isFalse); + expect(result[1]['gf99'], 2); + }); + }); +} diff --git a/test/features/universal_import/data/services/shearwater_test_helpers.dart b/test/features/universal_import/data/services/shearwater_test_helpers.dart index 9d180e6388..73f838129c 100644 --- a/test/features/universal_import/data/services/shearwater_test_helpers.dart +++ b/test/features/universal_import/data/services/shearwater_test_helpers.dart @@ -13,6 +13,9 @@ Uint8List createShearwaterTestDb({ List dives = const [], bool includeDiveDetails = true, bool includeLogData = true, + bool includeDiveLogs = false, + bool includeDiveLogRecords = false, + bool includeGf99Column = true, }) { final tempPath = '${Directory.systemTemp.path}/sw_test_${DateTime.now().millisecondsSinceEpoch}.db'; @@ -69,6 +72,38 @@ Uint8List createShearwaterTestDb({ '''); } + if (includeDiveLogs) { + db.execute(''' + CREATE TABLE dive_logs ( + id INTEGER, + diveId TEXT PRIMARY KEY, + gfMin INTEGER, + gfMax INTEGER, + startCNS INTEGER, + endCNS INTEGER, + decoModel INTEGER, + vpmbConservatism INTEGER, + startGFS INTEGER + ) + '''); + } + + if (includeDiveLogRecords) { + final gf99Column = includeGf99Column ? 'gf99 INTEGER,' : ''; + db.execute(''' + CREATE TABLE dive_log_records ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + diveLogId TEXT, + currentTime INTEGER, + currentDepth REAL, + CNSPercent INTEGER, + decoCeiling INTEGER, + $gf99Column + currentNdl INTEGER + ) + '''); + } + for (final dive in dives) { if (includeDiveDetails) { db.execute( @@ -141,6 +176,57 @@ Uint8List createShearwaterTestDb({ ], ); } + + final diveLog = dive.diveLog; + if (includeDiveLogs && diveLog != null) { + db.execute( + '''INSERT INTO dive_logs ( + diveId, gfMin, gfMax, startCNS, endCNS, + decoModel, vpmbConservatism, startGFS + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)''', + [ + dive.diveId, + diveLog.gfMin, + diveLog.gfMax, + diveLog.startCNS, + diveLog.endCNS, + diveLog.decoModel, + diveLog.vpmbConservatism, + diveLog.startGFS, + ], + ); + } + + if (includeDiveLogRecords) { + for (final record in dive.logRecords) { + if (includeGf99Column) { + db.execute( + '''INSERT INTO dive_log_records ( + diveLogId, currentTime, currentDepth, CNSPercent, gf99 + ) VALUES (?, ?, ?, ?, ?)''', + [ + dive.diveId, + record.currentTime, + record.currentDepth, + record.cnsPercent, + record.gf99, + ], + ); + } else { + db.execute( + '''INSERT INTO dive_log_records ( + diveLogId, currentTime, currentDepth, CNSPercent + ) VALUES (?, ?, ?, ?)''', + [ + dive.diveId, + record.currentTime, + record.currentDepth, + record.cnsPercent, + ], + ); + } + } + } } } finally { db.close(); @@ -215,6 +301,8 @@ class ShearwaterTestDive { final Uint8List? dataBytes2; final Uint8List? dataBytes3; final String? calculatedValuesJson; + final ShearwaterTestDiveLog? diveLog; + final List logRecords; const ShearwaterTestDive({ required this.diveId, @@ -253,5 +341,49 @@ class ShearwaterTestDive { this.dataBytes2, this.dataBytes3, this.calculatedValuesJson, + this.diveLog, + this.logRecords = const [], + }); +} + +/// Test data for the optional `dive_logs` row of a dive (computer header +/// values: gradient factors, CNS, deco model, start surface GF). +/// +/// [decoModel] is dynamic on purpose: real exports store an integer code, +/// and tests also exercise a spelled-out string. +class ShearwaterTestDiveLog { + final int? gfMin; + final int? gfMax; + final int? startCNS; + final int? endCNS; + final Object? decoModel; + final int? vpmbConservatism; + final int? startGFS; + + const ShearwaterTestDiveLog({ + this.gfMin, + this.gfMax, + this.startCNS, + this.endCNS, + this.decoModel, + this.vpmbConservatism, + this.startGFS, + }); +} + +/// Test data for one optional `dive_log_records` sample row. +/// +/// [currentTime] is stored as-is; real exports write milliseconds. +class ShearwaterTestLogRecord { + final int currentTime; + final double? currentDepth; + final int? cnsPercent; + final int? gf99; + + const ShearwaterTestLogRecord({ + required this.currentTime, + this.currentDepth, + this.cnsPercent, + this.gf99, }); } From 32b64e96bcd58f6061d2685a564b02331bf41cf0 Mon Sep 17 00:00:00 2001 From: Alon Dotan Date: Tue, 15 Sep 2026 20:43:09 +0300 Subject: [PATCH 2/6] test(import): cover remaining computer tissue codecov misses Patch coverage on #1978 left 28 lines untested: the tissue card's computer section, Shearwater GF99 sample equality and deco-model name parsing, copyWith fallbacks, RGBM helium, the Drift column getter, and a successful FFI parse that aligns GF99. Also drop the unused private aligner constructor. Signed-off-by: Alon Dotan --- .../data/services/shearwater_db_reader.dart | 5 +- .../services/shearwater_gf99_aligner.dart | 4 +- .../migration_v225_computer_tissue_test.dart | 2 + .../computer_tissue_snapshot_test.dart | 8 +++ .../compact_tissue_loading_card_test.dart | 65 +++++++++++++++++++ .../widgets/computer_tissue_section_test.dart | 5 +- .../services/shearwater_db_reader_test.dart | 45 +++++++++++++ .../shearwater_dive_mapper_profile_test.dart | 55 ++++++++++++++++ 8 files changed, 182 insertions(+), 7 deletions(-) diff --git a/lib/features/universal_import/data/services/shearwater_db_reader.dart b/lib/features/universal_import/data/services/shearwater_db_reader.dart index d09e6173e1..62292524f3 100644 --- a/lib/features/universal_import/data/services/shearwater_db_reader.dart +++ b/lib/features/universal_import/data/services/shearwater_db_reader.dart @@ -373,7 +373,8 @@ ORDER BY currentTime /// Spells `dive_logs.decoModel`: an integer code in every export seen so /// far, passed through when an export already stores a name. - static String? _decoModelName(dynamic value) { + @visibleForTesting + static String? decoModelName(dynamic value) { if (value == null) return null; if (value is int) return _decoModelNames[value]; final text = value.toString().trim(); @@ -430,7 +431,7 @@ ORDER BY currentTime startGFS: _double(_headerValue(header, 'startGFS')), gfMin: _int(_headerValue(header, 'gfMin')), gfMax: _int(_headerValue(header, 'gfMax')), - decoModel: _decoModelName(_headerValue(header, 'decoModel')), + decoModel: decoModelName(_headerValue(header, 'decoModel')), startCNS: _double(_headerValue(header, 'startCNS')), endCNS: _double(_headerValue(header, 'endCNS')), ); diff --git a/lib/features/universal_import/data/services/shearwater_gf99_aligner.dart b/lib/features/universal_import/data/services/shearwater_gf99_aligner.dart index c961f47560..6789172a6c 100644 --- a/lib/features/universal_import/data/services/shearwater_gf99_aligner.dart +++ b/lib/features/universal_import/data/services/shearwater_gf99_aligner.dart @@ -8,9 +8,7 @@ import 'package:submersion/features/universal_import/data/services/shearwater_db /// timestamps normally coincide but are not guaranteed to. Each sample takes /// the GF99 point nearest in time, provided it lies within half the sample /// interval; a sample with no point that close is left without `gf99`. -class ShearwaterGf99Aligner { - const ShearwaterGf99Aligner._(); - +abstract final class ShearwaterGf99Aligner { /// Returns new sample maps, each carrying `'gf99'` (whole percent, `int`) /// when a series point lies close enough. The input maps are not mutated. static List> apply( diff --git a/test/core/database/migration_v225_computer_tissue_test.dart b/test/core/database/migration_v225_computer_tissue_test.dart index e80756cb7e..17951e1b56 100644 --- a/test/core/database/migration_v225_computer_tissue_test.dart +++ b/test/core/database/migration_v225_computer_tissue_test.dart @@ -26,6 +26,8 @@ void main() { expect(byName, contains(_column)); expect(byName[_column]!.read('notnull'), 0); expect(byName[_column]!.read('type'), 'TEXT'); + // Touches the Drift column getter; PRAGMA above only inspects SQLite. + expect(db.dives.computerTissueJson.$name, _column); }); test( diff --git a/test/features/dive_log/domain/entities/computer_tissue_snapshot_test.dart b/test/features/dive_log/domain/entities/computer_tissue_snapshot_test.dart index 4da315af92..45cd4c06fa 100644 --- a/test/features/dive_log/domain/entities/computer_tissue_snapshot_test.dart +++ b/test/features/dive_log/domain/entities/computer_tissue_snapshot_test.dart @@ -78,6 +78,10 @@ void main() { expect(changed.n2Bar, full.end!.n2Bar); expect(changed.otu, full.end!.otu); }); + + test('copyWith with no arguments keeps every field', () { + expect(full.end!.copyWith(), full.end); + }); }); group('ComputerTissueSnapshot', () { @@ -212,5 +216,9 @@ void main() { expect(changed.start, full.start); expect(changed.end, full.end); }); + + test('copyWith with no arguments keeps algorithm, start and end', () { + expect(full.copyWith(), full); + }); }); } diff --git a/test/features/dive_log/presentation/widgets/compact_tissue_loading_card_test.dart b/test/features/dive_log/presentation/widgets/compact_tissue_loading_card_test.dart index 12a8597333..32c1573ce5 100644 --- a/test/features/dive_log/presentation/widgets/compact_tissue_loading_card_test.dart +++ b/test/features/dive_log/presentation/widgets/compact_tissue_loading_card_test.dart @@ -3,7 +3,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:submersion/core/deco/entities/deco_status.dart'; import 'package:submersion/core/deco/entities/tissue_compartment.dart'; +import 'package:submersion/features/dive_log/domain/entities/computer_tissue_snapshot.dart'; import 'package:submersion/features/dive_log/presentation/widgets/compact_tissue_loading_card.dart'; +import 'package:submersion/features/dive_log/presentation/widgets/computer_tissue_section.dart'; import 'package:submersion/features/dive_log/presentation/widgets/tissue_area_chart.dart'; import 'package:submersion/features/dive_log/presentation/widgets/tissue_heat_map.dart'; import 'package:submersion/features/settings/presentation/providers/settings_providers.dart'; @@ -85,11 +87,18 @@ const _overloadedStatus = DecoStatus( ambientPressureBar: 4.0, ); +const _computerTissue = ComputerTissueSnapshot( + algorithm: 'VPM-B/GFS', + end: ComputerTissueState(gf99Percent: 45, cnsPercent: 8, rgbmHelium: 1.0), +); + Widget buildCard({ DecoStatus status = _status, List? decoStatuses, bool expandVisualization = false, VoidCallback? onOpen3dView, + ComputerTissueSnapshot? computerTissue, + double? calculatedCnsPercent, }) { return ProviderScope( overrides: [settingsProvider.overrideWith((ref) => MockSettingsNotifier())], @@ -102,6 +111,8 @@ Widget buildCard({ decoStatuses: decoStatuses, expandVisualization: expandVisualization, onOpen3dView: onOpen3dView, + computerTissue: computerTissue, + calculatedCnsPercent: calculatedCnsPercent, ), ), ), @@ -165,6 +176,60 @@ void main() { }); }); + group('CompactTissueLoadingCard computer tissue', () { + testWidgets('hides the computer section when the import had none', ( + tester, + ) async { + await tester.pumpWidget(buildCard()); + await tester.pumpAndSettle(); + + expect(find.byType(ComputerTissueSection), findsNothing); + expect(find.text('Dive computer'), findsNothing); + }); + + testWidgets('shows the computer section from the current status', ( + tester, + ) async { + await tester.pumpWidget( + buildCard(computerTissue: _computerTissue, calculatedCnsPercent: 7.4), + ); + await tester.pumpAndSettle(); + + expect(find.byType(ComputerTissueSection), findsOneWidget); + expect(find.text('Dive computer'), findsOneWidget); + expect(find.text('VPM-B/GFS'), findsOneWidget); + expect(find.text('45%'), findsOneWidget); + expect(find.text('calculated 7%'), findsOneWidget); + }); + + testWidgets('uses the last deco status for calculated GF99', ( + tester, + ) async { + await tester.pumpWidget( + buildCard( + computerTissue: _computerTissue, + decoStatuses: [_status, _overloadedStatus], + ), + ); + await tester.pumpAndSettle(); + + expect(find.byType(ComputerTissueSection), findsOneWidget); + expect(find.text('45%'), findsOneWidget); + }); + + testWidgets('falls back to the current status when decoStatuses is empty', ( + tester, + ) async { + await tester.pumpWidget( + buildCard(computerTissue: _computerTissue, decoStatuses: const []), + ); + await tester.pumpAndSettle(); + + expect(find.byType(ComputerTissueSection), findsOneWidget); + expect(find.text('Dive computer'), findsOneWidget); + }); + }); + group('CompactTissueLoadingCard header icons', () { // Header controls in left-to-right render order. const headerIcons = [ diff --git a/test/features/dive_log/presentation/widgets/computer_tissue_section_test.dart b/test/features/dive_log/presentation/widgets/computer_tissue_section_test.dart index 9c62c3b772..093995b68c 100644 --- a/test/features/dive_log/presentation/widgets/computer_tissue_section_test.dart +++ b/test/features/dive_log/presentation/widgets/computer_tissue_section_test.dart @@ -110,6 +110,7 @@ void main() { cnsPercent: 8, otu: 21.4, rgbmNitrogen: 0.95, + rgbmHelium: 1.02, ), ); await tester.pumpWidget( @@ -142,10 +143,10 @@ void main() { expect(find.text('calculated 41%'), findsOneWidget); expect(find.text('calculated 70%'), findsOneWidget); expect(find.text('calculated 7%'), findsOneWidget); - // RGBM factors, He absent. expect(find.text('RGBM N₂ factor'), findsOneWidget); expect(find.text('0.95'), findsOneWidget); - expect(find.text('RGBM He factor'), findsNothing); + expect(find.text('RGBM He factor'), findsOneWidget); + expect(find.text('1.02'), findsOneWidget); }); testWidgets('omits the calculated line when there is nothing to compare', ( diff --git a/test/features/universal_import/data/services/shearwater_db_reader_test.dart b/test/features/universal_import/data/services/shearwater_db_reader_test.dart index f83256ee9e..a70623194d 100644 --- a/test/features/universal_import/data/services/shearwater_db_reader_test.dart +++ b/test/features/universal_import/data/services/shearwater_db_reader_test.dart @@ -8,6 +8,51 @@ import 'package:submersion/features/universal_import/data/services/shearwater_db import 'shearwater_test_helpers.dart'; void main() { + group('ShearwaterGf99Sample', () { + const a = ShearwaterGf99Sample(timeSeconds: 10, gf99: 42); + const b = ShearwaterGf99Sample(timeSeconds: 10, gf99: 42); + const c = ShearwaterGf99Sample(timeSeconds: 11, gf99: 42); + const d = ShearwaterGf99Sample(timeSeconds: 10, gf99: 41); + + test('value equality is by time and gf99', () { + expect(a, b); + expect(a, isNot(c)); + expect(a, isNot(d)); + expect(a, isNot(10)); + }); + + test('hashCode matches equality', () { + expect(a.hashCode, b.hashCode); + expect(a.hashCode, isNot(c.hashCode)); + }); + + test('toString includes seconds and percent', () { + expect(a.toString(), 'ShearwaterGf99Sample(10 s, 42%)'); + }); + }); + + group('ShearwaterDbReader.decoModelName', () { + test('returns null for null, blank, and unknown codes', () { + expect(ShearwaterDbReader.decoModelName(null), isNull); + expect(ShearwaterDbReader.decoModelName(''), isNull); + expect(ShearwaterDbReader.decoModelName(' '), isNull); + expect(ShearwaterDbReader.decoModelName(42), isNull); + expect(ShearwaterDbReader.decoModelName('99'), isNull); + }); + + test('spells integer codes and numeric strings the same way', () { + expect(ShearwaterDbReader.decoModelName(0), 'GF'); + expect(ShearwaterDbReader.decoModelName('0'), 'GF'); + expect(ShearwaterDbReader.decoModelName(2), 'VPM-B/GFS'); + expect(ShearwaterDbReader.decoModelName('2'), 'VPM-B/GFS'); + }); + + test('passes a non-numeric name through', () { + expect(ShearwaterDbReader.decoModelName('VPM-B/GFS'), 'VPM-B/GFS'); + expect(ShearwaterDbReader.decoModelName(' GF '), 'GF'); + }); + }); + group('ShearwaterDbReader', () { group('isShearwaterCloudDb', () { test('returns true for valid Shearwater Cloud database', () async { diff --git a/test/features/universal_import/data/services/shearwater_dive_mapper_profile_test.dart b/test/features/universal_import/data/services/shearwater_dive_mapper_profile_test.dart index 0401e98e9b..68a1df20d3 100644 --- a/test/features/universal_import/data/services/shearwater_dive_mapper_profile_test.dart +++ b/test/features/universal_import/data/services/shearwater_dive_mapper_profile_test.dart @@ -137,6 +137,61 @@ void main() { expect(warnings.single.code, ImportWarningCode.profileUnreadable); }); + + test( + 'a successful decode aligns gf99 onto the parsed samples', + () async { + final parsed = pigeon.ParsedDive( + fingerprint: 'fp', + dateTimeYear: 2025, + dateTimeMonth: 6, + dateTimeDay: 15, + dateTimeHour: 10, + dateTimeMinute: 30, + dateTimeSecond: 0, + maxDepthMeters: 30, + avgDepthMeters: 18, + durationSeconds: 20, + samples: [ + pigeon.ProfileSample(timeSeconds: 0, depthMeters: 1.0), + pigeon.ProfileSample(timeSeconds: 10, depthMeters: 20.0), + pigeon.ProfileSample(timeSeconds: 20, depthMeters: 5.0), + ], + tanks: [], + gasMixes: [], + events: [], + ); + final messenger = TestDefaultBinaryMessengerBinding + .instance + .defaultBinaryMessenger; + messenger.setMockMessageHandler(channel, (_) async { + return pigeon.DiveComputerHostApi.pigeonChannelCodec + .encodeMessage([parsed]); + }); + addTearDown(() => messenger.setMockMessageHandler(channel, null)); + + final rawDive = ShearwaterRawDive( + diveId: 'teric-1', + fileName: 'Teric[AABB1234]#10 2025-06-15 10-30-00.swlogzp', + decompressedLogData: Uint8List.fromList(List.filled(100, 0)), + gf99Samples: const [ + ShearwaterGf99Sample(timeSeconds: 0, gf99: 4), + ShearwaterGf99Sample(timeSeconds: 10, gf99: 31), + ShearwaterGf99Sample(timeSeconds: 20, gf99: 62), + ], + ); + final warnings = []; + final result = await ShearwaterDiveMapper.mapDive( + rawDive, + warnings: warnings, + ); + + expect(warnings, isEmpty); + final profile = result['profile'] as List>; + expect(profile.map((p) => p['gf99']), [4, 31, 62]); + expect(profile.map((p) => p['depth']), [1.0, 20.0, 5.0]); + }, + ); }); }); From 61978e52a27319f3558427c7350e58c386a680db Mon Sep 17 00:00:00 2001 From: Alon Dotan Date: Tue, 15 Sep 2026 23:10:47 +0300 Subject: [PATCH 3/6] test(db): ignore the uncoverable Drift computerTissueJson getter Drift replaces Table DSL getters with generated fields, so calling Dives().computerTissueJson throws. Exclude the line from coverage so the last codecov miss on #1978 is gone. Signed-off-by: Alon Dotan --- lib/core/database/database.dart | 3 +++ test/core/database/migration_v225_computer_tissue_test.dart | 2 -- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/core/database/database.dart b/lib/core/database/database.dart index 1d0649f87f..259406b375 100644 --- a/lib/core/database/database.dart +++ b/lib/core/database/database.dart @@ -814,7 +814,10 @@ class Dives extends Table { integer().nullable()(); // Personal adjustment (0=neutral) // Tissue state the dive computer itself reported (v225): the JSON of // ComputerTissueSnapshot.toJson, or null when the source carried none. + // Drift replaces this getter with a generated field at runtime. + // coverage:ignore-start TextColumn get computerTissueJson => text().nullable()(); + // coverage:ignore-end // Dive computer that logged this dive (for display/export, separate from computerId relation) TextColumn get diveComputerModel => text().nullable()(); TextColumn get diveComputerSerial => text().nullable()(); diff --git a/test/core/database/migration_v225_computer_tissue_test.dart b/test/core/database/migration_v225_computer_tissue_test.dart index 17951e1b56..e80756cb7e 100644 --- a/test/core/database/migration_v225_computer_tissue_test.dart +++ b/test/core/database/migration_v225_computer_tissue_test.dart @@ -26,8 +26,6 @@ void main() { expect(byName, contains(_column)); expect(byName[_column]!.read('notnull'), 0); expect(byName[_column]!.read('type'), 'TEXT'); - // Touches the Drift column getter; PRAGMA above only inspects SQLite. - expect(db.dives.computerTissueJson.$name, _column); }); test( From b7d925335327b4afc55782c03bd932f05aa1730a Mon Sep 17 00:00:00 2001 From: Alon Dotan Date: Tue, 22 Sep 2026 08:58:09 +0300 Subject: [PATCH 4/6] test(import): classify computerTissue for planned-fill and dive-mirror Main shipped a census over every Dive constructor parameter (#2040). computerTissue is what the computer recorded for this diver, so a planned dive fill takes it and a sibling mirror does not copy it. Signed-off-by: Alon Dotan --- .../dive_computer/domain/services/planned_dive_fill_fields.dart | 2 +- lib/features/dive_log/domain/services/dive_mirror_fields.dart | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/features/dive_computer/domain/services/planned_dive_fill_fields.dart b/lib/features/dive_computer/domain/services/planned_dive_fill_fields.dart index 6e2e63a37e..fa8fc877b8 100644 --- a/lib/features/dive_computer/domain/services/planned_dive_fill_fields.dart +++ b/lib/features/dive_computer/domain/services/planned_dive_fill_fields.dart @@ -13,7 +13,7 @@ const Set kMeasuredDiveFields = { 'profile', 'tanks', // deco and exposure the computer reports 'gradientFactorLow', 'gradientFactorHigh', 'decoAlgorithm', - 'decoConservatism', + 'decoConservatism', 'computerTissue', 'diveComputerModel', 'diveComputerSerial', 'diveComputerFirmware', // breathing configuration the computer reports 'diveMode', 'setpointLow', 'setpointHigh', 'setpointDeco', 'scrType', diff --git a/lib/features/dive_log/domain/services/dive_mirror_fields.dart b/lib/features/dive_log/domain/services/dive_mirror_fields.dart index 201ff59ff8..f28c591322 100644 --- a/lib/features/dive_log/domain/services/dive_mirror_fields.dart +++ b/lib/features/dive_log/domain/services/dive_mirror_fields.dart @@ -50,6 +50,7 @@ const Set kUnmirroredDiveFields = { 'gradientFactorHigh', 'decoAlgorithm', 'decoConservatism', + 'computerTissue', 'diveComputerModel', 'diveComputerSerial', 'diveComputerFirmware', From 8e1a7c5a11ab50a69529762f77c586c135895375 Mon Sep 17 00:00:00 2001 From: Alon Dotan Date: Fri, 25 Sep 2026 08:32:12 +0300 Subject: [PATCH 5/6] docs: add Tissue Loading card before/after for #1978 Reconstructions of CompactTissueLoadingCard from the widget copy and the numbers in the widget tests, so the PR description can show the new Dive computer section. Signed-off-by: Alon Dotan --- docs/pr-1978/tissue-loading-after.png | Bin 0 -> 84281 bytes docs/pr-1978/tissue-loading-before.png | Bin 0 -> 21307 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/pr-1978/tissue-loading-after.png create mode 100644 docs/pr-1978/tissue-loading-before.png diff --git a/docs/pr-1978/tissue-loading-after.png b/docs/pr-1978/tissue-loading-after.png new file mode 100644 index 0000000000000000000000000000000000000000..dca9ad3e6db59cd4a3e78aa3cbdad9553a112aaf GIT binary patch literal 84281 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 literal 0 HcmV?d00001 diff --git a/docs/pr-1978/tissue-loading-before.png b/docs/pr-1978/tissue-loading-before.png new file mode 100644 index 0000000000000000000000000000000000000000..b3c320ccfc3fc17dc3007f5e40b12f6e31e9eb23 GIT binary patch literal 21307 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 Date: Fri, 25 Sep 2026 09:12:56 +0300 Subject: [PATCH 6/6] docs: replace mockups with widget rasters of the tissue card Rasterizations of CompactTissueLoadingCard from a Flutter widget test, with and without an imported ComputerTissueSnapshot. Signed-off-by: Alon Dotan --- docs/pr-1978/tissue-loading-after.png | Bin 84281 -> 40176 bytes docs/pr-1978/tissue-loading-before.png | Bin 21307 -> 13127 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/pr-1978/tissue-loading-after.png b/docs/pr-1978/tissue-loading-after.png index dca9ad3e6db59cd4a3e78aa3cbdad9553a112aaf..9c0f0bc3a2eba77df637bb987b9e1b40a199b5ac 100644 GIT binary patch literal 40176 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 literal 84281 zcmeFZRa9I-w=N1HK!5-VL4t*l;2MH!BO$ocICOyE?k+)tCTJ7f-CY}(;O_434h?6K z|Lk!d_I5o1wyVqbmPKRhg+5@^sII;`LwAyqe@7hA524nEOVU2wzd;$OX2!V{mm^UG+WdU}S9CPiU0q+RIbtg} zV)3&{p5FRB4>JEX%yn&lji6=r;fE0E9J`Vdc%V^DP3`$Y!h+eqtFk?D$?vKEYyW!B%7?S+Lu z?D4rALPSOVvoZJnt1R(*1ydi1JSPnl!gLQMN@(KzR7LG>T;c+S*nq*_M38Y>p7t1Rb>bswIf%Dur)v zyjnE&m!y!-($LvV+eiQnRq;J<5~^?V?M ze?MExrpPHxzWbGIMBtuBr!}wjp!gam==0T5Zhn5~+v@~isMRV=WI8bEyw|?2^t3BH zE>ATzOCw^!a4l{wPb#1rhgtE((3+Gmhm(HnLisN8z@2_rLU6g{j-q3oR;_kHHl9HS z5f&+EZ(~g?EOl8vQwONQ8&cq%T^1;pyw%tOA2JHX6GXYK2Nw7FndTH$AvFa<;CQQ8)r%*8g$&ei4Mx{HZhE}=+_}*=U67yq;)rffK2p1rk(Txbj_OqZ7mkbD zQFFu{S=S6H88pq$=jJPw>Q-U69woBm73heW1L{EVjCn@jPCL%S?=~>Yp05(w0Hw>u z>*a2AhbKqL+#QekFk4KVsammKUG0x@Y%QA^>S<-^abHfe5)@Aqff=_pp2o6_N^32) z2-7SAfwhMDK}V~pP>20&mgDapx$v-fd^ZwtS_@9%C*M1=9=TCatRo6Pe+abYq@aMfQ zZ%l|yPL7L9jQ$`aBPAsxlku39ot;CD4wMt*z{SIjj>e6~jfviV3=Yok#bIa16IH(y zea6&=Zg6biqDm(dCM7K?E-4`?y8={k=1-DLC}mq}(iFF(C7Ex)k%D~no?$TOzk zr#Y%^^A**}-~QT3)zLwH3MVqn*B~;b>&3w{pfo^($I{TP{=x%lDUXefO-}qE6Dz5t zs2HWBsJ9li`X=hyt%`lzzH%ZDzn}a8pV6Q8T5%WXa!9z;&v2>lXw%{)YAlQnG7jK- z1wb0LcnOVs;2@{v;SgZ^5MW71Lx(#iPoJGvEMCRd5t^QE{TzaCr<6Imd~9rD+NFS| zZGma0@8L-CB*@-3XNoJqecLniR`VDY6xk-3zoJHQM|+0&3BPh5Gr);bGR9_L zmZt@B?=0{m?R5&y&+OQGqH-%;)l?&g-%?FoC_IJ%^-ubwMNZeiFqa|Xd7zlTm;$cC zu%hb5r_Hoa^tGy%Yhqlb<7S4Lf#Xkl7WC&&L~$O&4?nR^Dypnf_coywWkFx&y8_7w z$(cVfw>%nMx1CSR(>rCbLr||yC=-IEu#hO9;KA2)#Z*wa|Ns!LBy=0KMHA55t7_A$`o3yEsD%=S*boQ z)70Ypwx{=vE52KPb_7iIO%N$=F4jwA9U(LrpCh4QP$bUNT*|*MOO1?=?-fc@EcIbN z_?*@2m=GOpUBv<1qe)ehmyl|IwIrO~5_Xc+Opb*IdCG1P0K0>*$JbMT(^!j;3SdY7 z+k@y-qMf0cK0TV--@ZJxjPO%fh{wht&oIHLvA0eG?pCu_deFZwzY_mEl=Dx)3~9~&PX*=r*~l;0b_ z+Z)`|jNt#q2Us(>&m{hi=GJCHJk@^dISDIDvdDnx5bO}-rjKvlyD zNa#zrt$8mC^RoL91=D<@08ySQC?y_=pE%x^!I1o0dAS-L!8YK@kVaOul{B??$5DMR z+D+Yu^6*SB?Zz4XexA~rtW z^?Ldb>lQO-uBzF$rR3RJ4H8@g#XL3^mY}MMiHYdwXtlfIwRUZ{UutoGUJ}nw&E|6f z3&Q_P;Z*a8K==u{)!K*q%%BwEkPevei z)PS*Z?%2_s6Pa{E(@&ys>|`1`ns7qypY-W8l2l&FA9TQA|6h^FVtgOoYvf`G_VJ9M#P9&5%CJ3C^ikCtZL3fE(Y<}4J>e*mA3yjM z2Mx;r;pbGiw%g@Ce~$+=j96u=Kr_LYjG2XJdZYiygk05Fj#68JT%9&dX;dDpA`=wS zu(fpr22&35k%PG%tTVR+5EZU zdv`zSOEjqx+u&#}Q0oOvhA}OWaM*u4S|o&1b$und$IYXq)d@$fTP}E==L%))Zl1FE zlbHEl;@8?}rX*c&YnhEsD|xo*F0E-kwM?`yJ8tdvuaTom&&F%b^EvH22?dgtE8#Ca z8g-wuEa7gew?%FfbgG34O`lPb+fw%5_b<+71T^EEGk& z+bYm-shM~wOWWkx6Lz?w*+)~WE)F*B$b+E0VEvst_h-lKIi~F?C@|b1O~^rZfBRIcxjl@vD2XP@_PNIp;hp;yuG=R z0$tf1EBQ6>t4N-xM2d$+BFU`X-`fwA;G1n{81sKHaVz@LZBmB5DPZ%X>~;37Bv?Qa zGgIVHP{;GC2D=KEbOI{o#o6pTn%_-2gvNE$WqOXxo#?Ll>IK9xE zWU|!NnQxPJwi0X+BWsqEXwthW-#_O~>&e}22>;~{f0mx1FD)G^Ijb?12ZoE0onXIs z>3V%_^6i_vv^fcXUR9PTyuJ!J!BuD`FDkmC&d)bBdbpe1e8#%W+CQ*&2sWtUYswA$ zl{|`}s-jX|L$AF-l5#seM<%XFE6GvqeHc6JO{f_xE5F4oO4Eo59zVW3 zaoazi@0*+IEq9(7y~OJ^a?5|tc(b}e{Mho|gP(NvF;An;$H(WHg_p4!*Migm(F7em zonGcEK}2w~XVQ)mwd28}Q^#>@JrB=Na6E&20eW}YPolhw`X2~D`Fv-_F#GCvEgV*MKFbA{4AQ5q_zC-*a^b~ya=)>wy5N{ z35pEl;tE7@h54BjUWP_Y+=G-y$J=xxtY&BL)|}1)yzI)AEP4?)cKYxGD&lO z{*EiYilF}wkG;Ir15-Du+*h~&KLdmCA$ihljRwp)Tq|^Nqm68SzTtr*x{5lKl*3`w zc%?1ySBiSYgVm&fn>%)&^~O&I^0s38*;!gMiDgmQrf(sbw_FV0_TiL+irn%kg@?CPpH~-Q=oss-3 zySl<*OA)A!h+2ceS=XaE{QQeP^?;+p**D_L(~*lMlu-B6^Q_|^+PpQITu$*?*Rry* ze>mJm%A&UXlGqDWZ|pkn@ys4pAs2H-HFn|$2*+mHb*>8o1MOpDW}U%Ry>xWSy`&s^ zi76$Ja<+}8KGa#bPPu(dLPV zkoLUS?Nf}4Jj%2gaZ+Lxuav=xU{z&l*$vz*Np$p`VDx+z7@EedfN4PeE{e%$k9;#R z2IE|z!bE$SdJj@`;pt|IhnVX*X)c9p>9D;Pw!F$Mhz9Fh;6ry<-RCJ`}&PE<~j_OGns2Z}xR?aQG%d>L(h_dix~- zewolPjcW7BG!J4-8O(Zh|L}!qdNE9XkA-RrjcTjAyk=ql(~aI(T{s|$H4F`+!RMo+ zU~btWnc1?hI(=+OB~tu+xQo2db69o)eqwQ9OfPK~J&hEF@)1~eOL=@ECd7WN4Sk`9ITkWcpxM5as$B@vy~G&*S5}l0_A}!hJ&+2ljG(pqM9QmVEXR*_UsR> z9EMklubAv<%(u}(2{_9~)QMb`prf4`tLGcaOplqF;@vXwA45+!`eF{}%fhn1%}Ti{ z1pZ3ew9;vI_@(&Kt3q@vpdICccJ15H)x3lI_VqEE*Vw><-SE$#Hfr`y!x^^BW=bas zeY}$ETg=*7DEwAUJnVi>Ba^T@zHI-fgMxitXRTTU4~0<_>++$H-7CN-LJ0^!G3bj{ zQBtDU)weKG^>N*%+W0nEl2)wHHrZAD9Ssb|o3#3F`~?#)=rf@2S^CN);~nX69Awk8 ziqq%S)()R<$RO!)uwj8_RW9OT8eLhz6q1tYl5}|{k@h^n;vmW1#K_qEa!=P#1>4Lr zoSfwR)M)$%rr8TUS5q9+-@I>kf1}C{>2!GrT>SX3pLz08%}u^JIZVSr*XFpVj8$I@ zYjCe^=|_I&eHKY=Nuc0;lRk4n=_rc4{?+A$mO!2R#ims2+gdy7OGT#j@3dZrZD3`X z0sIWVH0^;1xPTwGZ$iApFLU1yx5%VxMd$x4KfU8E;PCpT~TvFE38!B1o9?0RAK{a>2 z?#TXRx9DfjwrNZs=FVJ^H^fssyuLV1b_d_asu3JQE2V@xh8z8|YV=XHpObNdQHDHB zX&4-xO_@cLOGi8Gm4FvRkx#eF%*PIC!cvj=WjfQ*-yuz@-J6v|PeGAGRjZ@SS(I<+ z_h#X}s{F}-Z)z&@$2Wkkxvox5KCHM?ZA?rV9vbkHbMFS`^)tE@V?yKMyM3mBefI0fdHve2DrhQSI&slYU8{K?X#o!+i?u z)3B0_xnIeAYjY)w?sZx{m(40;N~Lx^lDyILO$T>x?r*^%*HMcM{R32?q=QQvI|qG4 zivphau;7@i&iWXP8JfjVrI~!y#kvhSEClqRWc~&LyL#1hzk%?BgmAKjoU}Y4PO&hO z*%~J~Q#DQX!^MlM6_JLEbB7(Oge52uKK`Z_`O5Y7%;Z9#Ql=sXYt&r0oNQ8aFCJaA zfed|hs053mxRT<;xZ>~JY+BPyI@2uMuM$DAXeT_Ce!=youJOS>J|D7{*bI{JMN|hD z?b>Tw<1ocQU0=Hjnj~4AJM*$kX(TCLlQCsVyy5oN671x}rbDxxyY#{W+)BlE-IEgq zDvqT!1AP9%$@mXgaw#5!2l;7fC;OddD@}$}UG8%W?NV<;9z&?*Ajbz-2O8){=_0{N zyp=jtmtz|~kS5O|*`l#j{or}12@MVoj=rA0{`RAdjk3RiP!!o(LR?&rtk}nD4i$HI zqh2WeLZ(7fLJetMV^l(Do%s&4KI4avpaK7f#m<joI$u=Z(-$$-`)+yF zaeex4Pi_w}=aF%4L3elO*nn!uA_Wzt!jla+Ta|zl6YAm)x}Y{Z7yI(n)*q2HfXDeh z(ZBBRX&E)&V6vol zk*zJ%nnu+XptX{gnk(O^u~%9u|9I6%#ja;~Ag`bK)S-C=@_o&RXY(|K7jyGv%T zTt`xJRuVnnH&{>jRq? zeHD%JZFM24s9|x_4_Ys@wEi4>=1wXsIC4k<7jX;^?8Ts~G@ z5_%hb)(ibRt>Cq{+J(o_OV_(WboD~=6UH&RG8p_j+eSzrzg{CBrBk^w6DP~Nm+T1h zCkB0SY8*ub&5kKuZdX&-6<5J4+Y~HH$x1xE!ssk?6iJpc4XXvbhcgC=_c*!Nn_=bU z-)Gv?S+heM>^Hi!yTz0r-^khscJ~5C#MRM^$WqxS<~8e~smLg$&sIUPlTf6Sa|R8K zcrA|8y~w^c+Fmb0;JvwIH=PFtNk$ z3nQV?J=?&EwmeMMqB>M}*o2-MkX~OVTrnl_xI4`M)?o%&6C$A3(yNvVO z#%gxCZ~j2jku+buO1P>Q86M5-z&KD3CxGa%MoV3>)0<>lY!V(Sf2Nd<%ol`s*!=j> zIi92eHZ23uF;WiG4^9hCjH^+L(#b^9S))f*o+dgehf3B)N(z>;eHQ+Obj|U6=_pM} zxp8f&THh6xCMHaz;#+zx5C7l)x=R^W^>Bp=2Ea_uV4pwE3Nk$784?-~uLM zwTYcqdZBu~zp+5Ev0b%5arbw+lwB{qDFdSkz$+;-&QHj{$uX6{!wVeOqa& z0V&S;j<$D2Hh+`wI%4GNoj{R7)-%<&akJ6ptS;Sk>>r~P@6D=U9+1wXfu>*}OoCJ{tfCe<2Q@q}E_TwcZvviE(PVGI-O$~ckSLHQJAj;6|uU&yr;YT}4ce`7pZ zE5DMGA-7j3a4nc^6lhnwchT`7k(M!@W1Zma@?>bQE!L;S>248eh5X|8a4BFF4>Yo1 z?oF)Db;~6|8=xQ9Da|k6kfne`NX0`8A*L_QI4$2SZjimIHF{KdFkIl7*r_>=Zg4D0 zKw~y`2NiJJ2W=le&?xh{rfJox#wNl8tIf7nW4V9@ui=I7JdE6JtYhA3nl z53Xs#LOs-+zt3+y&MtW+vVdg?QF)7he=4J$ahW`XB2o<{-tTd`jd{%f*z~x(AaSl- zT7Hw{`fhS0Oj$J|xW@56#n82>60Iu`c^4#pJ+h9gU_75|Q zefjQY=adlp9ruLQq=ec!ig(-Ud|k-7NEjh2NPD+Y`_8h^X4Cv@*0FBY#tU7Ra`fLEi#}^Z!Zsf zjb;uWNw{1t_24}0N{X4v85^vSsIM0^zMBrZiUo#MV@9BXNU4>`A?VD==qO*MkyN)O z()7Sa0q2NZ7W&5hvuA!EgQ!CZsTvD41|Mde{5;DW%>aoDzq2n(>{z@IV06 zU8=9%LMJBWhbx3|q)b;_bWD8AVpz?O9pfAYD}$9}W3zYjI=YYrWS9>H@q$;g^25|C zj^-bI;pEsK}jd_He9g|4-xO*tx;nX1mT0R2zN3?ryq1auDcZH&gV2 z=ZgX==MTuOeEzq3fRBFj;zDhqa)F}FV{wV_O5c=~e;)K5m+3VE6NE9IH|(^&RE(+f zp-vAGq$Ttrf&s#&>3rlypjz#I$HD0L(bz?~aPM6Rm~EzxRlK9N*Z zwEYT#=bSCL{WDrB*xbJjNp48-3b+EkXdX{zkQ=XA@vC&Juw;=gX)U)e4Wm7MM+1)n zgx2@vTOK*T-Abay#u;upL#3rrvGKt7-GsJMp?DC=zficaD6&5T zR6$v}$Myy|;j$M(+(u5T&X?qLmotQ^NYBamQlZD+3;7t$zZ+XyUv18uL6=~6-baP1 zIVdq2Ly-DQ1ASPs_>a%qD;{1}^NnLCd*l^hdet}%6SfAwFM||y4ZT_GX9=lAMO8+3 zw@86Vy>M{+~j4z0lW=jyvO%J@Q^N`7&*0q;d&th8@3Q?8MdZ^K=S` zg>P8Z#tG6Kv}A_Dou0?ZoYigjn)izAg#SLn&nn~Cw{InjmX;PBKk8BSlb(395Zv|V z_C~2rRW;|W1$F|P2$zP2#B1I#1mx(VqOcM#@3FBhJbLA8nuec-8DBBCvxD>L;PM$? zFHC>@5YpoKMbpCBGsMr$)8E!L zIS#9Dn}W;`?V6sKb9*<+{|pUZL-#&EKd-E;93QXr9u6fvl*B^zP>tb;dvGIqb*-Qz z(q8RXbzE8WSX-)02VdTdNKDiN&uc+IHDeWDzI;XYd>I@PHZ?Xzt22M!tVQ(kYFiW; z8I_t^3Ls)6B>Y+blto0{z?bl=1cWCuART7sa!{YU9haK=suzG;@$>2E=CQvX-4_ih zXi6z%I1ZS$x$V5k(`m+o$X_{m4I;K+VXIV-+|nZ^WQAG+=I3vq}7u3!XQ9zc`|>Hsd3?La`5L;##0qTSut&ViWd zPZAUe-x0fO5Yb;hhj^Rs?CgwUqarl_Oivfq(BM%mrTuX84ENpJU7!}^=yFB!>&Qsl zfUxHXD34l*2&vwR3QW&MLX~4|_Hk2(pFkwSyJb zM@D|=x_*XB^bfqUHZi+D?u&UOKz2Vv5XAZiUuoe`cIqLsYmElb;>GoCJrlHChA;r# zH{J!nIMZG&M3ICq#9dFInw>YTBrky_TX-+_>tGHG4KN7<`P3E`R+WNbZ>_ z9gabep1r%Rtt8#mUM>y2t{;-%ivJVB=8x1V!_QcSYobxk7UKy3rK6*xUiO%3LXL>s zl$4?dl%n8`(BlE1t^X896P3FiS^osXM>u`yRt(Gr!h7_mas2-&#lc!8kdTN7@E0qs zF1BQ02ZOQDn^Tv615i^yc0he~H9mmq$$xu)jjt~8_pgo)tgt9APhq}|0g4TH@z2gr zU2bj{0Mk4Go=m8rR;CUs4jD%A%2DRe?3o_)VPl?yJ;T?>rWInf~4>6-M4h~g{jvO+(W2#<& zBEr6Y+o*q3cJ1fqT9xl$yx?~?6kD`Fghx77E&Q}Lmfm0Dim{e~6Q`%k%hRwBTZvNC(!?q?Yd zK}dN^`LAL&g@1Vb6uEO?HZe=O(diZ&8q|iJEP=iRJfra-0c=DA*dE|7zrROvGFOkX zn~2qFvN2jNU1Gf&%bCoY`n`tgG@b`$TuQ0@y}X z{zuU^WW@69g!K{Tyw>kXQOU->J6Sl|;D)*(ef0OF;&HL=kwSpHOe4z{$?J{`jw)t} zNWeMdg_Zb;ohAO0EdsFGK>lj7mTv3kwzG}kD>9G~;K1`}ZFXO4JUrsR3s00V(#JtA zUaSW>97^6Ao?bj|6C6kMVKWsIn$6WcI=4(!aZ*vRB_3W}^Kq@*xxYjQJlN}3SvvVX z7)n+l*_cMMq*F~&sIaZmm;||$W?X^qm-icO<8v^XkX9tmHB`}IZDNk2Fssq7wbiUu6V@A@Hy~!xZfJ)N>94Jp z%-bijnW-CJtD;IUY1KLG-cS7OF{Tvlbi+;=^)gydW&t>6L(LCq&9PkfYf_h3=eytjb0footaQUS99&8nN|$|HjmjfELUX0N72Yz2BMZ-crp~dVnsJG!+y8 z#67Ifs(|xQtIE1QQt{!)VIHt=$#Ua@pfU?2(0c0Sm>({q~_z8Nsq^h4) zR`ZU9+Gk2Xj#t)lRErn%ELfG+=(n);_KYa#T))i?dZ@h@u)bdJu=%*H!{qcKBRxr5 zDMUiEfU$B0lWLa{BC>6kR@mC=X`_VF`F738eQ zR5|5tyNw~`eofkFb#K-@=Z$s`h^?2^x@yCLo#?nXKHDS6%*E!u1=CkX5^%oP^5vNb z_E^M0?|wiO8h<-lqM1Yl;DeHtXN{hFi^n~=+IMo&#Gq)%R*;?Q_Ei_29UD8lA%hxx zv&l?eJT52#vAN|?Bxf7MejXY_|HOD%HhL|33^(gWoV?cWH8$&$qu!p5DUD~*y-pU% zDVc9{>N{Z}EgT=0&v~fQbma9qPVJEjtbjB*cy6CZ_F0|dIj@iVFD~#{gZlS(-k_ZC z=;Z4)&4bnt;bQJj2!(rteuIkpvB%{-`;ymAuMDioe8fD~H7L1w@IgBv5mZ~2fD?oR z<(+dpAehXjt(8mR3s{Y}{xe*9JaZPC(rgbtEVDC|e#^e!cd`mF9**bS0ed)1)A8gE zNq{Wz^DpHG?ooqnIeJcvC9i8gE2WF1r6uE;LIuE3N#+~4uE>Q{nW`yhsoLXZEj2!GXK<1{1l`A9Hsqm+sCx=cq7E zZOYFs)fiLoGg^9M3w5&Y$&|#H)yjf!j!~UT0LeQ}NRT3ktUbFlHbMK9lRkzX&a(aP zz~rp;YJwNadzAvv>Ruks#UKH!v)pY|2;G+aaAwzU*uwRA-`+&l!9W8xz`i4PdS`FE z5FJH&l5>=^^Cn=-3LU29NpE6yu(=`=C|{tO*8-zDgL+8J4eUV`9Apk(U2qBM2!tjC=E$-f(8bVA&&*o8*j!JW z0C=$fz1XB>U<91zYK(y_I>;aIlKrp`2P=Wd6d;B*Dtnj^l^7rB)I>pDi6&JkA|l6c>G ztRo~6qaqwf;lY>hw$GgC=;wMAz+m+zV;-<^?rocEy`!-@z%V9b!TNCnY1kB#gQRo8 z49AKu`i!=YI3Z(Q7d#He7an_TEF9@%b+Wyb7|7u1`jmD=@XqyTotyG3GCo%xlUu$O zX9$l^gY6EP0m**dylx&e9D{g!4{eIodirRJiB$h1Ar^_i-<(CBs{E-U?S&0rUI-j+ zwDdaP&Pc!?AjvcAZfyF&?;Q?L2imEbk2$sH`E~;Hecy;-_OB=a(31Py>R#Q7-xh|e zk%J-m+7?+)jYT2D#$gqs?tqsMnXixfG5ko5uH&ncb$JqtDknC~>)h`TZpX~SOZdid z-qd7QzC%HFbemjqU5~&vr&Ur)v-3x%TTO0&hz93IQNYdNF7j47|#ur=E47y)3@^!AfzKc1_%Jv@1a zJG;9=+(G;bfz^z;x&~nPuJtnW{+9fzgei+#Hl?|-t&-iTfmLPl{O4EuvkD?2{7_b~ zhmxYLqOhLg=EHh4Ze4N?^J3}vT%JY6JEQ~U)0)~ zW{HFS#`Jfk^&`buhaKOF(f0@w4FjnVW--u*j~Qos5*uuQqonT^uM#9OackL(xj{ed zbjbg`mGfBt)txk;r8WnW3v&uY4gmk>1wcMyyjhM=+Fb5~dk;n5@r~%3nsCNZ_h_NM~o)7(fUBN}G2|F(8E;{YGX-eds^EytoLsI1<|FJ{hUdn4klq4+H}J zclzRBDj*d9SO#4KeS?U;*XcJt&nQ!9X&VaF_yXnLD#xx7G`P$~L`^JeaF_wzus<9H zs}yG}HKB&P+8)_j+DZ)Uf<4JtqZ!pRG_tr^sTg!%;R_e72TI3D{EQ(cbwraT16J$Z zsSMeDZ^xSnK;OBWaYnNbP;KkhXZi>~rV&pRjAYmmPmYyZI=C8LlJUC1FX1FUR$BMa z*QA{=fdMHo4%_IUSW@s%B%5e&jf24tMRCYupKr#RN8!_=fEnK z_Fct;(_o5W+~I5;DZGb2xcA0=4kBN(3I911JuDRG#B^9F^EiBau|J-_10CBW`Q8fc zqnUX}$Xm$#*l3RsNLQN(@R&STSVZpnA!R2=URV_gP4$%%qobigVNaIOLZ)7GdAZ+* zhL8g44gJ{}rU0pVM1*@>VBMFS5U$g)HIrne1j;h&z-$(4Egl2#?8@(E8|)7TA2N(? z5;Wq8&Wu7ch;XjfiXP2S@s!jP%KP#@KE9Cq%Xwd4DJ`*)Qe-x>a+(*_bzideo3Vl7-r{-GMv6K36FFo&Gge#M6Iw>V|ww)SwPE z(go|4RZ8fUkY#WFEIXajaad?@T2uUfSJw8nsbs1C>U3f*X9(hUcRhOc zkUuou-@!#g5sK9*F9Q|m^$;5zCR4kTEt-K)iB5!D#3wh|uceLkr7o9hOwcP}gU@N{ z$24WIUq47p!R^&{6Za_qmS0v5gbZUAEgR+e~{g(e%FiJ1&s=|OGKQ0`+p%6Utp zAz;`G0dzZ_&hNeicARu4M(OdxwL2l(ceZQQ9u?HTdK+898n9_E@(T(sc1L9)s#}-v zZu><0O&@k%PNtiYGo1T6U3#xr{;WlP)asZ!S1PY0z1i_0)M)jwuCpk)Kk;v6{(LgM zU+3P>9;;BTLxXLmhzWY?{DrRVKcs9ac-}p<4=Y5upI$5M?&7bV&aJcf7O2oA)IE;h z6m$n3TJ9E30kB_h)p|Ue3hewk+Ywqz&(MM@J zg}d7uBc0P`P~Ptt1Dh3@pf7p`CanSjoY-=cgzw(5IF9ZElvZn(laB&QZ9`?t2gJwA zf6wfoR>^hF+Dan6WY6c%yet5>cO~!c&f3gcMdW+OvFX>uK~GT?zmPhN1T^u zv$`CPzEKfpT4`?L_5k3XlaM4{1I^Vw@BnZmtknXZ!e2Z;f!oH$B$-<%|G2Yg5%;t= z0fa}cxcRqK0vwz!D+?hJWbMh)%1;BQKO1c?p@P7{b&dJC4A^c(Z286|li{!Ki>k0? z5gSJvym0>*$*gxiF~@GCk8lZpEv*HA&Zx+!o1|D<0}?g;#nlP--2tw;B7-+}tV**% zw;sJ@^Cj({&1`LZn>i1Y9G_iJGMJRD}qTN9d4&La@+*l7AJBWV-2*9QKA0hn(t z1o!J#(F$Gc%D#?E4HL|Bx8CW{1TtS?z`nj}9Vl?TkbKg2;=uf=64wzR&zNW9*lIPEO&SQGn#e13>c_;n=(YlPW&F1!B$(RyLhT4 zOMd{@`9tkv6|qw@Uxc~oPp2c$ah|HPN7$PjW@}_TuMdyg0^m+>?gpCz@3ryhqYVn+S7+?UFrR~+#ofHm? z)+|YlF|Q1FcO<|Sir`t9?)7)o7VEOGxY~4Wa9Zt#en2st&vEm#DNxQq0Nd{V@9pE{ zpKz;D=&D6GwXM@Y6317|E_#BgP^1o(&VjneO@L(HRa75aH2X8Sq3wZ^e(Ed_YTCrh zQ8F?-{NlySRWHBp27f4d9Gk~Gh>-j0x!&R`_+Gnhq@LT_;Vh2D2#pfJ9;X3EyyeWp z22M#K?Cpm*SuR@IKkUNoZ8hd|?Rd)3u?+pJveI4B-1wcJ z&UY3h)EsU?)4rTGC;PI3CJzRXI8-fx19h73?fx~jz9$kL8He#?P_Sbepm4RdZu}b@ zEunYx>Lk1_;hyU}3G8`8viSuXSs#|_c8;{K$OO{#S2>TkJkP%5LJB;KOvPli`5YeT zCSL4K77BYgN6$L#^Ey7B*xhc|Sg9p0e6s+E!*#6GCt~+T1KQO#XM=#zx1y|PXC${N z=4N#B=V21=w{bL3WP80yGBx$n>)IMWBdx;>ZKp8BBKUDypm{{1`ExKETA!4XLi2m?}W!)qB z&TwC!BXf{bz>hrl5JU~tCEu3Zje0w93qGg z4*GjQ0&Yap4{UOJXo1+s9Nub1yFO~Mr1T|WS!l|i#_m;?S5B82qJ>~6M;?t8VBq=A zXxl$eA?;vZ`^FqPi!9U4Ap?*fOmh4kn;ssvN+W*gh^sxnYbO>cHNw(&mw?JzqzHIb zUA3?{Dydmg0|vb1NF}+M(bXTt*EU;AUXF(>KbQDlW$U1)0)9wxPJYqDl=B3l@d7vy zn%wV3<>q9*zSk>)^3aunD&YbZT}-+E<7l#UI-yA*Ss9{gm6pV#F9QG7nzo-Sj8?FE zpuxTgB^j-m>&4`Czkdeo2V?!12&Ea(&>jAJj}GsCGg&z~NR0uu2TxN&6_ogFEY} z*eRx@KMU7Vv1q4eyIP_%FNBdh8T8Dyst2Yyz7v|#x-)3RuvAP(m-(_n9Jn=oz_fJe zbHQ)AJ&4|GR=DEjmz}`1m|oOCeM>rKuN=l%4!#oS&-0uSfph80Jxw;P9~teJ9i z_+i_*&)|QN{ryI)W{=_pbmQh@tSX$DL^~`wovJAb9|jSGni3ikt!$<85?ImX0j;5=zM z6pk4~?^);k`GC~o&Mc$gdP$pW6|Rxa9Gc@8nb1^_D!@Qd0FTzkYF2R+5KD*VF}5$M8(og_?6H3W?*77EwD4Zso38Gyia zfV2jX9L_}z65$*3=vX}Ocq|s_ZUx@4l&ZGZPt>{N5aGXj{rWG~&qpW1is#O^?$&my z3M$3QWm=n3WLxh@!N03fs~Z(rLD!e4*nRMnQWH6g%{Xg9j8s{SpqDihB3~H=(uRyH^fHY zQ-=@*$XUS92KFG#TD3%7lXs1UGFZpfBuW5JSHQ*#@C{sw0L}ro6iu@=(^WNg>KNxGYw!0(ND3n z-i?CCjJ14Cti?L9A7apzn5lWaUbGTCL0u&P9$CDQ0R0zk_*<^ya}pttjqsD?3BCi; zXa6_-e`Owg{GMLa>xQG%=EpBCnf|k{YUxdj& z^MunQjE;=~5GR0hJ`?`9V@Qc=2ABbW9nf$DHzb4g!EvQr*^TA1xvus&-ZxlW3ovUp z?$p$!6$6m1g1r24YKurGumC*6eB`%8%FbGvoaTpwhhn(xANnzSEQtmAK6O3aM$A7O z;Bwvyv)cZJ;qIp*D<>rPMXnnjY;nCod^})rj1{!$8OxBeXJpwj-T2={z?1=Y4Rj?WNqDRQr${p>4Ak`^?dP{HbyMez1{4m9)n zW+MtMbr$&QO7kl^7y<~J>WyF(5W($94z%QnBTWTN0U~GbHD18pe`t0TMtW%lVD{HlpT-e8R-tmnKtY6Nd>`hYopQ;;-oPvM zeug4KD#-9D4L~Wqd&sZb|c2a0)eKZjfse)0!mQW5TL{{i<;pKA#&p}9XLdj_Q<{_+w~5K zeQE|lsl%6Etl~lTC;q)>-~X|_ z_bmj1g!dP{*l1~8zBMNUVRQe!d4p>UocObIavJJgMy+&n($jr@z><@bivd4R>@STf z%f3aV?%b8ll{&k=l>hSp6$J%{$pU+Wn;N$>ohSQl`&{>tQv2L5U$T9G zVK55N?xQQKMkXg$pXSLEp$yxh)^!aFUTMDQe9((r&(nGRu+;tX;`~%XLgJ5r0Rt*X zZBMpIU4LmXOosS>G56L{QNDeh!oEL1lV)kDGSM{h4e1ZOa5z?EHF+UT(Sl(aLj}&|OUbHnuGW8svk}AC#dY~E=rBN9g8yCl@DMlNn_;Y-G zxX!zTBKJMx4l?&83r#x`$;JPwzjJV;WlYgZ&us?WmBC&hI8{A<6s)g(r;QRU zvZf=4Mgr@l(}g9qxzamOB4cnPQS|ISuaV?zq%*>r+dsjBMZ$wE0#p z|M-l*w!~aDT42K&mG~D=)doxEYCL`6Xg5sE! ztQLy(_VFvAHa*o)Kmv+)yWYaQZEily1Jxerc;f(HRdc126_UuL`%21h{p2!x*cu_$ zV->6Z)9y?Nvvgyuhd1-Maw8vTb5qqj+nOG-?@EPA;qBZS4SSOKtlLoVnCQ6 zC@>tzXTqeVXG#0+XqhcQ*G~RQN9uXE#b`B46?}JgpGS%{|7y`vT$4CkVh+FzrXsge zb~$s?A-_)tsDe|`(5q%xzDdPhSxybNrVvlijD%1L#JqD2SKu>2KJU_DcP4@%0QU4W22yYXb5DBbB3>VNy+*3Vq?cj^s#DDd`1l!-{*W0oC~F@ zq_AXQW%2X8{L;+OSp*FyF56UaFxU`rF9k~CpG01#2QeJZqrfT4Gy3UE+$3*DxWoKf z!kLp5P)X5p0GYx|ExY;{8ens2IM9S>#0V6^0~1ORC6FS0(zPvg`dN_=f_pUL_szx@ z#Fq!acxhbm6PBm`r}RtbvB7=3h|HbcgK@A28A-uscfUG4T0SCws8$r6tI7VPd$r)I z_dY7Y*}CTt%i`SOtdxP*VMYAF)Gc|FS8%mIGk1d5bOyY@Y3s=Ahkh?%7HMj3zVnjC z&-NZ~b1`4BW&IeJ71{u+6*Cy()sX?5u4yf;36BLUf{0M@Fz(6mBvTQT=fR@3Q_t@1 zL3&k&M!O;tn^8lXD}rhO#}cE8pcPayMzn!f8kiDK3>M$lUUZ*$O`KLaul$MULRCv( z)LxbK-1!K!`ILczqCJ29ykPuI8MwNW3Ga*-h=d)*XitACP$gMvav*nU;`lx4i58H&_s$MLUlC{XZ0MJ4d^r8l z!LQX-Gr&Cn_9JdcKpx~G8_~Kh3FR@DmkPm_xuZnB3=>HHN}@-yWQfpF&ei(aF*4}6 zHQTWe-<({zVM?U z=FAXDeJ|OL+s0o5rT!ROb!^&XdBz$4qrO3*0oornxAZ(@QmNpNq_lJ`%MoEbYggC2 z2`{3Mwi`9Kvhw<>;0oHEX=J;%?goYK?uD|F$1@FgCLd|J@RIg)H7ezaD`AveGs4=4 zzFN0@gw$#mLPtr$m6cRdlbDG6L4Vlm+H}jVT4HyH*j%bqT0)Z;mC!%8YP46R6`HLLC6P&W! z&wi`W>-r+Ck2Zzk*gbCxY3eyh(+e2_&!yaNerh^8#9g5ea9jJMS-Uqe&)=m-l;#HF zxijD|rtKSpO3{rQS6@EJSta++f&MzD#T1X}DLiR?%fQcL?+Ztkit*fMU_i84?G#1P zkQ4w9L>mt7;*#{VAzUw-(_LYS3}a>KJT-eXw@$IyF2{Crb_nbRI~Ug{rOOGg#!Df0 zjfEbm{;|CKY4~v*)`aNpHIj!FJ0KK6^BopBX=sco*T>YRN{+S*5FH?@U+*GS7svS{-8`- zu&9-E&vSdb{RbX$(h=GgY>=mx?prgNE@AKUrAq7qm8;1~la}kT-!!_es@_r`hXt9c zw(J={A}3kjSnQ5^FS>Bk!<|IX)Ax5*7^-mmGqBqd5v~H4K?vHOU@-sr_0Al^2V%KV zZEcGVZ-u&G!j1*m^ut`pwo&DMM%~W+i(Kc>DG!82NnbKZ&5*&+lt6ch?bPfvIM zw1u|38uq&*Ir`?QZbzd)rIW>I4vYEPDlVVfM|Z!xixK}MdTRPNN2@5)#lf8koZb@q zD$r|^pT^(hkBB6)hSa@#b+W%SSgtpj6-Ec*m8i*T=mMoVyP5pb;nKs&`;IGvPVF5B zIWiA>dtwNmeIifR_bKu_x|qyHe~@)zji1PQQk>sk=t1|-%m3q2-fulG*79S58c-Ji z&8*TSg;fW>4CcAzaTU7d6iK?xhUPr9U;#R;ggzsCvGQtjP& zK95_*%;)-P>9e4Kch5UZjhV5qhsP@xJdg=~r;k+=ou8tji1yd%!aLim7wnkC-}YXb z&Gc+V{CeYm@hXdbYrI0C+;YG2aO+fx`x%K!)RBd*T#3z$YOW3^{E$#E7os3h-aR97 zddXs+WD{F`*U_950EM)31I4ARbjmE+&k4`{vNMh|Un(g64eE=M9utzb;@tqsdQ(%= z%uLgpiG(Hr*ZI+dIuIf!O!sbY-+S=2**_ZAihCObi~9x!Mwy9;agF+gg|1uuJcv0{|= zl=y?Jtg3=Sd(7BM1u)d&;o&lL%9NCp020_|-PKg7mfLBIzLm=KuOD2wHpm~}XO11k zd5^i))g`^`ccvredzVL!q4Sfe=@`88DiUcYMlltWe^hTIz4hXW&Xd#VWg!wXGf1!$ zCwp%?`Ss(QcAxpxe7#frr!XE=A1aEB8h>~)-W$kYn%wq!-GtsU)bw?72EAEKe``U@@C2KQq!!iAZQGPjxH7&yP} zCOY_SA`mkhQ;LDC?@#a(fE!(V9#VlG8IcFo{b(GV`&ducAs&8)DFBM z;<8McGvoy4b%zY06nB;vG&LiYwo4eTT8_NEeR_0j6%^juP8LBVC9hl5f8-ptE|cHB zrZjeiM`qfBmb)9!sAjy4?L6}J6Ytb5S_I&c$St$J;+Zj(hYNPsyCA}H_>w;UgOXCR z+mIw%dO)^(Z9oC6txW>7=OEFgGt--05JBkCWwMlN&YX3v8`MjY74sEk-bLJ+Gg*-s zh|mW1hPB?Z8m)(5A(sM(r-HX6WF@}?6$*Pycz8E>u($-^RqlzoDm(hq2NI^ARcl_^ z;z;b8BI%m>)j&Rmu>%*7x5D{sZFhhn^3!DI(TS8ALem#Nq0(;g64-`B#AWSn+cnf)*o4(8eiJ`;}`g6Y`M@+KlmZ$Y#5`SnxI)E4!AI z6ef|I-8-X_i-*mj1TKx=y_*gB`HTktAv{wlyK-B>!5YYtU%yH&g1(}CQ)V$0;hB*^ zzYc_Q9Pk@6XeUZD?0-@UW4o|!N>+U^Aeeebd>_!$G% zv%lIFca6*3;!JKvhI*0r&~)u28E;Lw5!(0?o}`do)#qG zISQ%oF2;tA?=-wlx>T=}1y8eY9bLA;bJ{)1AWEx!+WN*^YVUPABM`)=I3n(nYH+1e zwDzb2eO4!#s3joZ8e^?Ftn@0D>s?KiJ6~XLEZ*(&w<}=k%Qmlis@iwQvh+7zKeSKFtWq!&*XSOr>8(^h~3JCN! z;i56*6M%^bz0`YhWj-9^n3iG~pp($jU!qA--q;JMv zF}U#HP=AAItz>1*ipcD#H0j*`Qa$8u=ZQu@{1zS0dLw{TA-{j@HF z*!}ACZrxNHG?3x|T;gXEi8sGa9nEQJXdKrBLJzK`@f_5Xn|uy2x^}3+9RPe-AXiCM zJYgVK1^v#D1B>wr>$cBmJ3+5hkWJUPJMkR5mcLVXdGAy|+&X;KKJZ+bVbOir+zMSk zNAhR5=_wQlTp%u%T6L5cR{aTwQ;q5QXwT!WY4^i^tV7|fu+mRjYDyw=xrjAi%QOCJ zsE@xyY9@&NQ=}zShIU?`!b_Rn(j3kX0ec`?z;m(a`_H$NQ7Ya@rPYn`8iT+rJm6wi zR#9dZ+N!Nn>t`KU%uhPptP7{3L&6k8-GETFoVx8-W*-azH;%Bm*(Yeov|67jmH?k7a)eZGrS@DWAk8rrMYgrR(q;U(ji{ zn)z6#!pR?_?LEB#6yTFZ?tGPOS~s;EjNmXetmvm$LDzk$wW^DY%~I~SGTN&8R)n`zRdr0N|A@F&2vjyQ@L|DrMUKhP^ zyTv=VZhZxb%*2EM(B*PF^;>wZ`hv{RqyoAW6i-Q&+F&!W9{cwgmElQ zkEfgUsejAYF$)oznVSmf==P{~v`J`$|m-Ftz`(fD#^O$~c!9bVAQZNT(qfd88^YGM4;jdwr( z<=9PXwXwJ@cD4(4(N{+3i2xT+mw)mr?Ze88mmEO*ni)~4&3vXlt{UI%c>lrWZi#x! z4DvY^F@8*Udyl+wq84Emb-N=rsQ@Ie6?BUm%4bFw)Lvs;99n7=@Q^d=*0kgA?cr3f zMLg-^;NZ|;HxHe}efHvA(!E*$JFtk!&N6GsxPg+O3Gqa`PP0#!T^Xs_11YitcS}}z z#e{Y#D!gI(tbX#h=eRngjltOHCkvDH4`m_}A}fnyBe#t!GM0rroy4C*p3HqT;myx7 zGpP40r5CVn+&~u_F1VexPslAy{S-MvaNg0XMR%?vV;+Gzp}<*S+xbYNdl`F%TJ#w{ zZ<=I#Nx2Usmq-PnmEb;%O@BlSqf4UU7Ns?(#*Jlf+9jK`weH5rfPwy3@|VBogG&uP)ynWJzqY3e|4-=7i-&)nHxxvUAL=Kcl8*b%yr}NV*M-A zkP)D90wfQ*)|taf-hV5-e0^nuN~?bg6?Lmvi)v>FNpR2<^Yq`f01;^wmA(9>*Pvod z{hk^U6=~c_w7;0khUT$8e4|u9)B3c?beGV%-bMG6l9bKvvAK|~8%WuB%*{=WY^94t zs_@SpjOT$SOXoto@UTdQnC8sb^IK6-ibv!}&>@-V0}>L~}Vr5!b< zCO@V>V;7bo+Fcm80+#xpYkl9TwJ;O~9P%wbufXr${ReuQRr)|2;u%u-8WUma{BPx5 zh4BSi{K$F^URYQwS9~Cv0lQ{>u`c^n_Y_x7+S#E7ApU8}uw2D39rVEV$9g@wG_1g2xpI%Aw`X}5$L{V98xecY6l{-xCuxqdYmtg38>Gsh>)?dELM0=%jR8pD zlM>J8e7v1RZ0jl5M}w!U!`-LKy}+D41v{T;WnzBCsv%bvk)+@9`SxM7iK!kCS;YYV zw=<*FQwj+QO>+%9WS)ovykXeCE0%Y0{)BEzdt!7NU;)+g6%*B-*ZaIIBH#Yg@~%zb zpm;~*YRvWpP&(^~zJEWsM1fZ!jbJe(ldQ*Yv-{>Vfq>dy1KM3g?jBB6+LI%bfm~9z z(#|WsF9%c;x}pZ_Jom_|T({dKPWuK2A9oiZ+l{G-h1@=b(``+elt)=?>#3*R=N*TF zc6f>PxPA+{)N!;xmB(v!FK(LmVy(7!Zfr26rKT!;;e5gV%S!b+TlD4H(te$sBopvx zW21H~MFIg=AR`q_FiCdfm?wLePr>~rAeNwY0n^Q7gxXMA0BqSpU18>BcLO8Z#`cWm zj18cXj?tz08dZ@cG|f}zN8ayuGehy85OVAVQ!Fv+bXk>vmRHrKj%Hy4+;s6dJp>`c z%wiL6EJG^f)+AgTHBfDd<7DX%kSCq-Y(9=bmmV#VdxYZR;GBz_tZU$uIHuJRsD0~f zdomA_%llA}TuMsrU*u$bK2^~B{NezMe1bo-oDbIA0#-B?1(i|;h@2;Y!oI5DP8D-B z$>{(W3ukBkAhO-849JqNiB-z~6WM3@qN*J*KbF@K(E@zHA96P5Ep-Cn9 zjqZVJUWT`|VxdU$5%KKovy5`$vl9ZWs$D|!qfJzzHu5^ByJ1K{%iGd5IWRn2r_QqR zlv5RW4^Q|tZYRuY61+V2B*t^Yi7%htKgS}jkV@6kT$|m{eYx)2D!lt^@ZbB) z$;!hcYW0c@{NLl3x6utBaMi4kO#57cf3-OBAM`@Gz_0%Q*UkN3l{{a)z2XMi*8dnrGZ#%MS}))BktsJr?4W=(Zh7S}Mq$w5?p$~EfsV^UOGW~(Qw{GJ&MQrE+n3h0d=;cJPDQIIhRc=*#8=W#gFG|PdetKLO9fpLr zD{X_$B|mic7Dp6ER-+)tzdwb1H6$W*e(kCc>=qmhVRRn=uXbQde@uh61Hc8?>}R4_ zl%8`}x!H(z|2D~otXhsG9D4u6kW_!ZbQ%5ioab6=@z|I5oHaKJm9LVUR2q)Yr>~Q6 z+w)PP!Kw1({0|amIr39WwFa{fbyixQ)NS#v!PmPtlg;}O6n;)h)Vq<(tGG&v9fz+! zNJ&Y_$t~HvN7|t2;7ohsIor`vm#vn_nHdGpw5$S3T(}(FeC+|xWh-zP-VpYD+kEs8 z*q`}=^!OR#w(vjz*z#*`eK<>*eZ2kcaq+{8?zBhdfB%U1A&Fh)lt_07E?v4zUuxGFlQ_3J+uF z0$Ew1JC`N;>e7p^hTZI{WG^A>hn=ncK;$#=`GN-U3V{LwzsCtm=*73G5b#-?nY#Xx zS?9NcV{|qJ?7*vJx#YxvO~SW!Uhjp{mDbFb+4BYYFAya?uFfk4SQU@`eLQ#V!_xgJ zry08q@HO`1>KvVt;l`$V8iiUb_%f?Rgv$u`9-Yd!bU@XYd|0A+M9%Lj8|cFZ>(L8f zs!?M(qi$K(uuzELt*4y}_VU<5MRwdK5lqa2m;Gq#UW}1ik9^XayJ6hEDkmbo|l4Y(Rfz@aucw z%Qb0}@X7acW;i2m&;j zZh~|bU{oUEL!LQ zaD~JAwZ+ck}(7L_aN{fX-fv+i_-WhYfxbU3*Es zvQ#=Zhqsrmm}3~-QO7PhmBS#@=`_GnnKRP_ARIaqThzpSy9 zV?5TW^@LAR2o5hUdVsh4^SRG-^vi97Z4d@2daYJrc%H?p*s_t={Sok!RoG6t411Us z6-i)Q^r%D8I-Rp?ti&uQDe1g>)N*5S5_b_e{Ly9N;-#fsd^kmed{E(@C>1*Smddl~ zmHN3Qy9$<2H)I&2MDS2wpXd2Lo{J4lm>J5)0{vb#80bw&R{NR>Dphk9riDRDQ1bm9~x!**BtUiY-BQ zuT}G@@_o`^x;y4+BG(VvUf`cfa>AyQmXrmYB(_wYG0v>Ma2g|G5+cvp^*Wrm>NZ!Y zD9#-FOSGo)3Jz8<^Wd;Std8I%{ag`E&+dLL2-So=>j}`0f`a3fni5kuHjWaNt&_dB z_M^uJMJ{l&M)bk%XyA8{$~`g%vt;KO45RoZxv=ulO`y`B>9%BQW^jB>lGv3_ET|YH z=H-denGP2{xtO;zpf;5u7V7L34o2zbs}8;NsIo8W>k2XAe4Z=c0KP~BnuL~sdR)PW_Ts&`cD(8(fQ#zE;UE}4( zc>r1eS@eSXv)33>4*_1Q`K0@rvS2;-Cj8Uh>ED}IE4)7OHjP z{yomB5S){HqVkeS_rFWvFH#N${RlFO74*EB(^svaJ)nBcbv$UZ-*$u>8M&1QhTTC++~?qmH((nTmoxIL_4AoFj=y^IAMb{|-=oci4K>X^^A;x zh)2a8>Q&L!rlNO%g33)SV{W8JSw>LP-cBjIanRL7)f;m|nOv{#i0A7dGwbIara zTtF*NhzrAJZ8!@o`*y(U1|D%`&orzAxJ10?LC3<#Emt=**tcKQy*M@w=?L1TGpWo2 z6QjKA;J>l%TpEnt<*Z`ByUJ+D%1i{{DQUmlx*SP-%djwnivb?ZgC%|TFSpV|8f%mt zjRQeuUqMYsgkQh#fz!(Ip%Cs!n{a#(=(J_jI6BSMN`dJE7ip)31$OPpjYkd9KOXRq z>P({zCDO`dPc)zVevpu8KPg1B{FvKY%7|HbU@um(0ggU?*g@Qb8q#t0nzP>TynvYZ z4$rw%IBx1XKx|{WS4)_aOTWC#rm~o&!pO9qPA1Dd^4Tmk)QtuuO)iv^l8YDZCuJ3P zyOf#u7hOyS9i!k(otiq&C|<)Sj8AM+|IYct5RDf+E*FOwQZ-;v=W1l;WJ$Js#-tSs zisFwyV+79s{He;`t(Yagp0r%ef7b$#SZo7wj#lJ#!W9xuHMNd*(G{628-DVATzdwi z?)veq5ci6JUDqABbqrfwD%f5sTwV6W9BGcHT&LgH5AP&e#E3NVp4Xfl|6MbidI8pV zIU!pYR7~E(ULRy+s+jnE;)7+ZwY659=iE3pHgZnn&%n^KnXF@x89Ci*yhFr5^scDs zSwQZj~)QZXv0T)Dkton@L6pqWNTB)Qgy>GCqlI~N85u? zVsa8(5>V!)t8=ZWr(GT_Kbp?w>KGPcrsro}YV9{s(BkYrt(}Il%5rjyr%%&WW!c0P zxVA3O6hGCwn#7c|+uH_oIi=)OE3i`pfoXZwqDXV-Go3cu;-MAecXF&1wc zgFISC^gf-+SmM7OsnKeA+#eth-bMS;M{t%WUy|pB^`vA}!bD>ZG>?jQX;b^vwzLk( z1iKb~gZhACADs0xw1N&)>cxLL_eMl+X-%9cp^6Mt_zjM8V8V}uvPX!MmDYG=EO#*N=tsw%7FifAx-ZZ*%|eTWOdq3;Xyuo`t(Y@ zGENcB?sG|Hl*wKv#@89m=`_?rS`2uq6CH~;KB2Q%!f>poJVF~WfDbp@<%PkAKgF)u z%N+eJ1rL9vLTfIX>ZcBW4j=)|<`EfJwlvK+WVACZEia81c3M8_KYV(82fX0Ax@uHB z6h2*oZx_bHW!7w6`$|NR0J9n^@SAtB5Rb&#A&gQ57j62 z$JXDGDSt>ilH`VnGn$%Z^h9t&`?9)#KNQ%Fgqavvd3~pNPbcglKP|@!us5C;%2g{| zL^V|H9{r7Pv}jN9bZ3OeN!W8VV`($NRK-E#O!(N~fV|E3F|hWf33!CHIlFG9lK()+ z5DgObml4CC6x8zj(?(p9~*V_`$RX zBUqh20K^SMAX<^h2;FXQ7tUxDUHHq|Bj`2ef!S~oOAW6Zz2$vNxn?_Tm^7*7$$>J? zgZOy=v+(}wFuh3VzQ)VkQrW-`B_TPX&Umkg6MMsD*HKLwK3O}p;zhfozgxtm;=hT3 z4iE7EX`3e^eu>0k8ZAB_eP5B7*5Z54a~vru+A8ll)FvOVD21J!q=rj?o0P2-(BY?b z>AE+?!Ad*U)Ydz-neA-aziy;Vm|D=CM&A8aUs3Usx_4ZjY&WqOxum}SulJB&d&v3c zY2R{^a)5IOoXmkFxBYyj-bhC7>09-Ri}3W1$Y0f2-xaZbzClGmOk~IdX@!N>U1tL4 z>`dCM#_{Z2*Ygv`d|q7=>rj7G#dHlVtL2Hu4U=8*JHvMpWn`@Lu-OBB;|ClWWRSvY z45Ks<#j9EK7VX?n4!>L^TJD8`w8)xK?8x5u{T0*)C;x1_TiHmkQ!nS4%ccS70lAO!1*X{lq3tnK>mu)B7Edn9| z_P^>B?vjp>|G7@_Ip<@5^^Dk-CNN5KQL+9LinG-EJfW@iA--S}xn>OBXv5O_^Vb&e z{^nmB(2f7=Ki@Wdmje$)@`o~rmai{@d=a#V-Q0L#10DVEw6z(K6q5qgn?eq|I|p;P zxRUCc0FN+URPO4OeXxh);C9+DI9M*Z@BlC#A&kF6W7ZcIq_Et?+;p&o#lZ0+#Js%k zV=fn0BMXL?jn;YprK$~QDU`emM`A)BO%)!gL*Kot(HP<_wz?i=CY%?`bG5aMHo511 zxHUGIq99Pw4C>}4-P&&J^E?%ofTP(R%K;G;g^Wm~W?zc5@Cdpz9Ia2))`GDXk?|hQ zt1_C-o5?NTUS0qdPt(n|ZxqMCI9mbuTY5MbQtl@$k)9e{=Di?ixpU{vOpTiLB-&&9 zjrrLf=sdXJFNR;edIiQ;9d|V^SCs>5+kfEJGK#7UkVN0-rBowNS{1|HK>K8Nb~cj* zpk}xjNu8;UsG#f5(x1~GDCRl?atg^G6VO!Ij1vU^5eC<{0@yDZnE>Fo4vMWfKb`ag zyCo$hB^Gh77Xu0L^E%Tr&(-wOq$GU}4I;IVx4mH#Q2Vvl!Q%BSZ%O|2B&BAVrs^h% zm!Qqy8P{M$Q3|r}?-evfWDeF?SAz>&n%(fF?Mn@gZtAUCP8LakZU|6btS=4`JA+ox zBv4*Cu?Y&!EPqD~W+>$LUuwgEB#PZ(->}kkW|u>6JGHq*F0yef8pYUUWpn4-Z)#bq zvwtD4mO)1vq7gwO^~Q-J2XLmQiX=Q%>lTi=83n&EAe@j^k~O%lemKhzwrGXNb>7zM zAEO;Nx@-9T2OkPSEEFfI{&4N}^we&727|F^0KF$bLBYXK%gDmxbM}*r-wli}GevEp z)7_o?Ma4wHXpHZ5uCu*Cv6`0?b>;&?ZFZBTat1P-C_58z$j`kQWGAMDPeDCuC z&_07RMwKaQDnkb=@5Lz8<00m@6sd=kH^%1%CQinmXL`SW{Z98Fo9_ku&~8! zKV9F|(obUr09+bRHk9*Z$N>`F(IOXLqrVXnB9AI4ElNSt(sr!pZ~z%lJ#Vbj2G4se zpcZbQ6SujcOLr0Cvzj%m=?hMm}Os!+%Hx&D}`Ye%8qx-gAy|3!bCZGxl&%nuCk2T~E)N zT&UJk&x*D>yWD3K-V7BIUHaw3nPa=s5eOLN113y`(O`6~9+uncpI}0C*s~VwY^_2P z%n%9i*}mdpeb(lkGe9@D?Zu!`8qU$dJ%Y17{Cdg9%S+^>H1m7mHsdpc*R`bh`1M4} z^`x8`J=c>lT-UTJRP9l~>AwSb+~onMJB@&dqtIBJvT@8J!ZRBB1keG&WT29oSj*ff z6@XXZep~~_ZjBZ{0(A20V)8jNaQMemxrCL#8DnT-eCefZcXZP{sMB&Os-?u!3D7}7 zK{Mk3qIU28!zJ^YyLa8jIdh9jX~{^R@fpw3qbgQZz-$z3-<#3a)0=^60-BCK zZrYVb)&7NlmH44$jpxn?Qha!n#`a-&j-Jm9(B0AN<~d0IDBhYbq(2xNQ!i}ACgd0$ zX8Md_OLtm7-1g@)vizQCV*IVn56B3N<5YGPn?&o+NiU-9buNDc@kfobtIo%&y|=coadz6}zN*iKg>`UTb&W_dHxO|o^bpXlSW4Qpxv1oj;tMr2*_tSK4DO0k zZK?NMZ>t})aXbQChJ+TNUWXO0XtI=`?^-RdYTy9gE|i_!j6bt+vU zPL_6ViEa?!Ozz_};UzR(GoApT)`@2p;J{PO%M>T!SLp}S?f#FXNk?;!ZN1=&Ja8KTc^VF zY^2?OFau0V$k4+$zWhZYRnUSt9B>l{dQmR@?3pZc49;+%3A>g4CmHDJJ=4pUU$ZCvRU>MfKEN^q@(L)GG*Z*Dv&Ok zJ*yQBM)z_$FQ%Z=&+DzaqpB?C7Gv9KXwJ`#3^&)?B}X51e*5+$HlL%z}YbtU+fqZn%v$d2g8$=5h_hYWY+q(T3I)T29*=Lw{0g?}N_B8Rh*t#$6 ztU4nSJ(m5hL!PZ9>T-gt_kf9(+TZ`{gl%1NBuDEEZqu!efl)*FyH(KzwCK33`HDuN z*{XTW7V`M$!D5X}Ww-q%h!a95mI#os6x3Qp1Da)DvQ`Rfd9j0o4h&A`;9&Tthxn&` z4(t#5KSM%}ffzC#_JS&VI2;()tGo(bfzS6~dml{sqa-nl(bcIm?0m0xZPIyvQ%7M& zyz}XlT`bUc{=*BvJi+?M4c_yYPObBg$M63w7iNgf?C*%s&|j~HQ2m`<4Sg(yQA9+N zk|QGEyR?OYo;m;mPgRwlo?!xAuNt*lnnk1{=neAlzoF)y@2>F^ti9YP)y{Lsv@;@= z98s5e14+jfx)M%LXZqBoLaPSQ+3j|bDf5O*OuA+n+1U_?hRqFDATtE@L$IZ|@2)%k zQ0{2IqzElSm@3w(0@)NR)B5x69ng^iI>0w?-t@}=aUN$E@Fyr;kDmU#er>ke4Y-qM zMP_$_NS7nvDUMG{g1-DNsUW46hb}d*(}EFVo^3>{DV^gvDbjBW8ODINKD1|ZW9|B_ za%EG^nJT_EkOdI*TW{t_kbDk!XXrqpcH_D#jjjJ*Xa@_~KpDzqF%DnQ&W+|+9#2jN za}LpSfb{xl$T&P0D3wqzuFTO%nyspT8=5|^OoMA15mjZtGz2=mbUAaBROw(VTmTBp zQGxmGDF#4PUbL2t;oQ0o(|QL7zxn%{#Qr<990Z|dtUGqn-66LX9a=|Qp+EmJb4mI6 zd}sb3{DtDj52ibiaJLn?`2Szi>$-c8$Fh+dNX6gt2c-BOJbmD-ST%y9xi$^T2*V9NPKjsi-9uhYFt2CMM8) zD%8m>Su1jG^k-TETAqK_JUDQcm~=KK3bE=sfk)FjvrMc+5_lDAUC%DvpR1wREDvYh zhkK}LXcYJa1;6C}z#p$$%LmgeeS_kpxxnaA%15=uBgZuM>h1$$sY28T?_N^;NX!OG z;6Fq#X+jwaO{s@$S#zM`4?wIgNJ>K8r)4DMt<6*gMnH^5bI_kRe!enCeV`Z&jwrwn zQ&wRUdm`Ym6MiQ8H2DILkwbAu;0x#i+u^1Zb|^6U*F>c}wF5f0yDCnKii(ZNvtS5! zIr+!dO)ovYCpnXemj1)CTyUnS8mEP?NE7T`WX_p6Ch4@%#&X0+nEBQ07O%7Z)=F}f ziCnzR$@*JC5-_Rn%6$XhysEqsA-0!>wUS){29T^TW)E+9M;C&a)eV>A0E zpHOkLhyGTHr8zG8SsQdjzSoE+hD9u2vYW1C_2y~&tm^<6oz-DAHU=fSAb9$;<9Y(^ zzNV0e=U=JiRmf0Dh#q&q%7ipqj;EPeNW z`{sw|wvpGe4~Em;8(LoD`X4gIPaoRWp~&6yNi7 zox*A0kFfQ4nhHv_IoUuT_(Yp|xh^W+tB5xD2o;z{Tb&c=GY)iqdLaw$)G97=1aV&b zyZ@7k?Nm`TV7fb1be`T+PW!nwxJtNt9*{{KtfA;PDOtxNA*qbnEigG!@1x!U=H#9fyJUjU!muSC6BH1mDA@~Yr|62Nt*f05pMSEm-0C{)!P)ls=%2jvU!NOF!$JZ39DMA>GeZExV)z zJ+jB(#S47#{2nj0E+@}$pyLg%((CU78n&?)iLo@HW~l=WiVzpKfrye)Y=8V;0sZ+0 zeYJhBd=P$Bk=O!P0uZfdZbo0alEHycpI)OR^0$nWDk|~KRfsmGEVlqGIU}R|iJRo5(X8dvQ)&Z%}CkAvlVYEV*h8}4th9jkC;XtGtAR0EVc zm}rmzjExlvjd!;%D1@Fz2$dT@a+$_d3w;6|{wr2{{!N0MCIE!AAT+<@Z7dDf1ndf= zS`}zLyR8YDE0GQlHuVZIN5~w@OKX}hHa$^o7D{ag#8fV;armU$r(mbn)8^~&1PVgc z5*0sm#su;Rh;Ywh5f5Q^i6ICmna~tm>-4Clw@=r152T}!5d=am^G!$cthJsy5kMoj znC}1Wo5f((L(P*eB$ebSER+1~^vaHJszr4~V! zpekt6a$66$*D-?JkJjG>w#xQ^!SPL?!S55zVf6>3B&oAVA%ECMd3l>p8XCMl+H>nn zOVY}cAqa$^=RQbg)^zSfz25MAY#U{N1{_e2MelU_)d!ZT4iQ0J?4ycOu!z{ zSs@i1%TF~dl%d;GfuPLJpnk}g=`aLq;|6s1`Gi8cDu*6U>XfHQi-5~~=)!L3CollyH9el(e6@iN(u0OZ3N;puUEQXCr!$EM##5>_bXi4kcH_Ey*aa=*N@C7yBOUz> z(vVk$bb1^H4cp-|t4dc4t*rnlXAubq>1Ewkgb(FNwM!HsKk9(O(a}tKv#)}~QPA}= z>P%Gs97Bi8oXBzYjUu4`Krv5G6PTWy(k3#6@(XMO#{S<~m0C{z8xtNO`3;=Fe>-|X zhs5u`hIS8i#QWm*OH#M(BE|n7l()GIMppwt7ClHHp#QOi7LVr>bX4)*wSXI6Kp_s~ z4I>FYK;Q$>>0eT3P5tb84x+i~#~I!@5_5pfb&h^&^o1hrS|K6FsPV`dU@1nIGQ z)ndBBV=%rjDT>it=@$s&@XWzrydvL{Fp3LccQrQ2t#Eg_k6I}RT(G1Cs7P zm)H7S5*(4?Oi?nU*V1$dqwtW{bv2w(H?O#Wc_P7zflbLkmCf_-*aKBfP5&`4E+Hpt zYiZ|^G?10ZVB0gsA^9j!odTqyDkaNEGl7Z5)|FNAceJgy~xpebF%a^(NP%j??9dqtnG zOZ^*04w;xGZv1+2UiYXG@l(an$3jYd&x=Cv?j6MR+QwWf(??J<*U~w5m@0CRk{lZ? zXi25xI^AEjy8ws^#JdXCoI+rZIi&wvFJ~ElN%+F}p9G=Ldab%=W@&?%*?mXj(pW-s ztr0^;Vf1x0!|3*9n2(f8-izMLl{BpuFC9WBpxr2_(wIbl{L zDmDNqgt&;u08)9iyrCFJGcz*)!6jx)_B^-Q`eHCxz+}qj*(oo}Y19N5195dwOKTI& zY-1GxZRw8NukO_<7cCfy5S8;M`2k@;T6!vCd3|xFM*rgQdom_y4qaylFJ8#}+_jvP zexjtS3%71<@rV-VU=)noqNmKhx#!^UV`pP|nV?p{q{Ta=VEb>U;J>T0D}S*XOCyFA zk@8eKOL@7K@cgYfJN{@7N{0%{|8fVmU*d3+PQQ0m0X@2PLavvzq{5H_yMeSYSVm^n z^|fqA7W}~_c}nZN)!Osi{dMyXQ-p6^US7|V^%5w>8zeKeDmOMXoRl)NOTGD3hho$& zwLyW*92kJX##v|^@ZZ+r>stZ21q43o*w4K&Sg21Cyaa>`tQ?T${bk>vfo+sONzZ@(ILw#$ai zjh}wyU{fZ`(=9VL_4`GCFa>O@+l2v;e$XTdCgq7B%G)FZRjomS-t^X3^Yr0-ou+om zZ{_f=NY@u+;AK(G3}smaCC9GjE;i&Ha9EZa4JTq2Nf2|74l!Es3GRew{}1BcIwv^>l@sze`hzr znF5_BcX`S*>y(~LEEFaeSj|LxZCXwponx8}>~ifVxqDWPdw{8vOJI+$<+y1~&|bNK zR@3g9skgEFuYxG7s$@`+YWvO_yr(#a3KQ0|e+T}n0W62`)JD(4Wu??%ah()xzPf#` zDo_<=FFk69$xdS1tNSrKapG|G!QXkmC+?XEsFNd{qNC%pHt$_x$kanhHe~?_X^J3W zkSL7NH9kG(^Hj~t00Xr3T^X1s){j~qCVtxV{~G}R|8m~%zaMi4naf!!Af&<21i+M4 zLrC`a`Jbs6Ku5@f{v8`_P z2h%;_w_vTL(XDfI9(G;7+6U~7UDa-H7(TntA)`jsKNg<0gSbb(ro#I@@5)d{Xs#&h zBSGs?QB>%Azr{%E()08x{2@NW3U&{)a5G3Q7FulS z2z1;YuY;sBs!8X5sAM|(NyOCvL;x_p`4oRzSO*~VA_!0{H9$wxFhk0ou-#d*(~Qdz zCuK1zhs7M=(=bK5;?0!H#Xu}A7%+W(MZif3&9;9>uaHh8fsZ^CFmYB~jZXOhtb_cLYk5AaWHmFqk-(@#6 z?TUY2JPXT7jwSRSd~Wz~rY*X;UAw!wi+>W^-rWZK?(IhH;)9`(3-Wb}FV9n}?sK7M zB8vdIs8q7hFhhyRW_&yY9;-qE!43_zf{)rjSXYyL16{8!fcAg?5(tRQ%p8@pwFf7gT<5>25KI+c4#pN2 z6@exd(cN-|THgu*-Vf&M0sG^oBRM+N0Zt8;4Q{T7(=MU-eD_mjQe7+$D=v~JMevez zp&gLx0dat5c)bnMEhnD`tIG_o7hftN@$Sk_;)mEimnOHgMn@=s!m|V6s=(^7;IuuH zdMiGR>QQiTJqZi*ra}!Tp3_uOA>WKuas^S`)!@juTiw8$5)Gj8JzTse%o>*ta@hOZ3L*(U zZ;k>&yUlI09o&=vA_>QxK25gRI&Nb(l7cDt1vdbB;vpx0W)JY*X=%$?*)Epd=(~_` zR*n~!7mrn$L3dPWu*N3c&n98pm&st+-$n$V%~G59m$s^ z)zx8pW`97Eik;zZonW&MyQ*NL`*$~=$K_AI&s$^Rp~26?e{7)U{QGXjMo3nrB_oSU z$1e;KiWY$ks zn(KMv%NgXPH>QvuBt-=PU@v?*lOsfrnn7L?jCMWvIdt0vpqU8mFKopSpwR^TH_a_D zt0-Y$`@kCf6EOpnGS#dv!MeuhamGN$djjvWSPew8cueO?E1c%jdllV zc0s6iB~azMvw_m?B^+0) z_YG5HW@af+ZvC8oKy;D$4(y%~I_>;|j0V&5Lr^K@wYevj-Zd&f0Xj8O8eRFgDORyM zE$R;iyw9WXE#3}iq^93jt&@GE^Swh#YxUeEd6iR~Epw<7eN}6ENPfyQ)~FqV+%v=t zs_$g71VRBF=L{M$(N#0U-!%`e8X}Kj5DVOEotucHhxjMj0U12OWa&dJ$uAe`tM>T5 zGEj6mJU1L%i{;pG_#x8MefMH(?_vw@whmOQm`L9VaTc;rf|FD+Wug#rDjnq z&}MRIsoL`cYV*v&|HfeioiMU8Fnl|6r~=DV_2V4~%zh?03F=A@UEZT8-Q1WLuO~{U zOXK?nlGhg!-TFC>c7bJz&wfY$lOfz^k$Y^AkUjiP*_c5KlVbLA1Xx!#L8oSe6GBhU zM&Mi>93_8fD+hLd_3FRMMG3~idRNc{@Dhu@&mvbq7T@pJlm3(|0T-{T8wBNDr5f!R zjZaK$Q93|?_%76PvKj7j z3FRC0@xJtC=Y#H4up1XtX%>2#EzfLKdNsS0fU6=dE&v!=fbRmD5=-FR;<-8p8e9h= zk9q1+e7B?6G)+5t>|TzuL{8>c2e+WvTG4? z#OZuBisHg3wGX^RIh@k7aq}A^7KY?U{;WMX92~JyO2gW%b~k%%@ag+no3zV_2OXzp z^bcRgb+V$-V-9M|Fyfo!ph`V!Vge#e5tK154fngH1Go5>c5QZf$!8X7N~(8o;;O6k z)Ue#6Zwe#KK|zuo#6YnerZ`=-8$qsh68Jx`fVnX|yshDskHXf>d7#`C%e}x>BCrcY z4|Q-;zaNQoG_NtA`PyIRr}dNSN@*ev->b=GiPaQbm08&a)N1oBh#yaMdq6Ogqf_)NPGD7Ak*es%K?|qCr7wk3il~xHoJ<#y22V{c}T#zA%>O%22G|SNa z3pe&MC#*z|cG4!D(>)^Bf187(+3T=!AQcGnvJ?xn^5gRIjy3>l^h_M<&)dJ#P0pEf z0hQa|sZFckgm2#Rdo2_#iog70&#+KOk59z!w%pNHf4S5zhES!_3>J8sj?BJ@7atk5 zi*~JX7TLD_uQ3T^Pm1gVt9(kcXY_o3_9$Kd&lmmXK!#4o<9+Zk^8y!#gt49v&W?(< ztSpxM0S+p6E=Sp(SbN_fQF_F$V7{S#Jc<^X{3LPw#{F*9=ISTCOLW-NJX8j0XlPTC zIZ%27d#=mr(d(zW?q(gM4S50Fvo_DeFS{oqK5o5o%HvfXpml7>wJ6R~hy3B%= zNu`M5{~eWQP$o>^DP$+Jp_`c21WYZS8JWTMwx$=LB&Wc)*?nwC1ueffRY|nqK%#wW ztcP*QfN5S@e4+5B*C^E2v}64-Pe@3eYQjE?L;SjIU;Kl&>2~)KHhAX!IbeyJMY`d} zaa2aPJ;tYJl7V=9ac{9aHChb|zTAV;9-j97yXczdj*d;;Zbu|5DFM(b5cL!|1%~fELcFO2)k@XtG_tw5rh*={MJPaP!rS-C z$wD>rYHN!cjVcHne@bo6wJ9Vl-k%_GGSms9mEJ{v_(8EO!$ndup3+fla;fFlLk4)~ z49HsW*tt>JCyd5>!dOIl$7}@uySVwcZe;mxXg{f+o;3MGDGO0#o8#F~g!eqqe}d^! zF|!7E+Zb>Zl~^|}73G!xxG|eab-%#wdvj@9@_}Hc{a_F$FmU%s zlX=6kx^m*uB7V+H7h@gXRljpWD`{&;$x1Fxz-scH0~@>5LH)5;U;n0TrrhOKDvI(= zp1@O38@(0S*9%OPar4-p!ISU~1?=QaCNW}ubo8IC+K9c1OLNT(GjOopynTh6a$T#i zggc(!4|-g-IC=cvpk~}?JMLYB=aYMSzFb@!KnnxmUT2g3-m_nDh6gcWF!j-y7SG%2 zmU`{tZg5V#-kYsny@asJT3F0xF~{>*eh8^JlAysxlM9bFurd;2Q=_Q`hlkFz9AC;_ zm;EJKasCkwf7dsIO^_qI7wPAI-1!s~+8oWJ$j+4u+Pw3MfqQ^we=alED6dlWo5-uJy|%YPsD^^HeY>N&G(@!NUce0Kd7 zkdw2PUq$OO4O6d7R@TiIi$ACT?q>w+^>=%NNzlI@2z*f_!6C&aTApBMq2i1=S9vV3 zp(h|EVSTz~kV}N2s+~6+A9aw@n_csfjT6>N2!K!|H^5Tb_;KT5-YI=^05KZW=@@^< zzOCj$?gnSD`{GtkOd~Ae!j@V$ATjn*mgaqRbmzewM3`KUAA2NPG1j^_DKtOZZ#8Up zYCvxgkSoAt8KP=qY|^O0pi9wAK3i}3ySL0ou&wYYW&M+x1`xpLtMM*0T|Y}^N{H?% zL7zPbJn@NvqJ>KFpI!>K*~&xQ5MAI28be+q>Nf_u@>N>>!o8Ngy1K_WRH8R3Q6jC6_BgV3yT@^i0YiKlUT$brU-Kvy6H7cVa|UNhufTdQ{>h zD1H*hxG@y5LH7GjG~8Ww0Y)1&&puwPZG~u8+WTUsY`JlwfAhn;8mAoOodQwUTUwZg zi?aOMa6~nr5%j;7mo{T8I(rHWJe1<5my?O#a(z}yQG*9ngo|BZfilIfm~;`{XXe0j zxp`czhAcMB#LRBkn>-j6(Vw4@Be9!5#c6)x>AsXMZG3{Ween`LeL2=MsM8V`MO7;Q zU$>N~q7r83f80_r>l;q{e|O4>P)q|2mlE(mlY4$nPQ~&noKk`2zHc9VNLe3(p9}HS zShIL_F!7&Mjaz^mmapBMjnrzaN-<%+gPsfuZE1K@_!;b>YQVN?2}s$r@V@OM&QcmIWOf$C(Rs}X7mK-tfk{we0<{S zu9b4VSV*@MOi4-07+LwQKMrU(llk1ph-fpyKpq3wo)LBbwM$tVH|z;x={MEpyO=R`BqDhJ(VN$cZ~43$u>Okp~{OMA;6R-U10C z78f{Oz2PcIR4?3IuPMkW!0C{fh`VWv4=cxhjVB}7|5uPP&?pCt;t}$I*G(}I>@RL# zH6|w~uSR<}CVtAy`Q5j{S#|RX%X__L(1`@X^%nW^AMt5($KMUYcJ0IOtOcG17E#ge z?rz^865m$M@zU&|1b!cx^)0S_goH%}%uO5Rz}%Eu?&^E#jQYe*#zYK~q+Gn}jS3o_ z07ljDn2}?uB}1xij!ygXCx4O+|C8n)osf14gCTKvI@dC_%e_Kevbg9v$?XRCoqzmO z<^|eR{tNcBg;r+rPjp=hRX+S5ff(HKxu;KlpSUSK1atbNKLG_S;NPn!|6!Q_|9SPf zuXe$)RWR}*`NcRt>iZ$=`?vY|#`7Wz7x1s%q7=OD(ooR6`;g%%9nZ9On$BfQ6v%|o zSh{OH^xWWcI7sC28}7K-fDH2iX8fOj9aR6PSD{E`qzlC&iI~L1+|06W@X15A@$f*p zTTFbs#Z*;g3$K*7ZvcSFxITQy4o_)XTg7>WlP-b5#x`C`Kj`jcWn=v%4*Uq0lDP(2 z{`bs_GJSo0KvlEk{Y6S@2R!-(BUzGs4>8mKya3evZ>@R0DXVK*TH269NvXR}sg49} z4K5%`=<*J%(R`5g$vV;O#ToWZ@6?`QT@o&*k5#Gv?>qvKuAhFe_ ze_+6{P)$zUV?08@l5TJfd+qkd?`{r~()Y_zH#f?QnqG!I!M%Wt5HtDLts=0(*UVP@ zka&soJzqSZ-Fdy#5NNKM=pT7AgFo@o+#=MZb^&BPn3|hc$Z8VwPlV)Qq;rM`GlT!S zn9jpzT@t|ObA+C*j)#{R9E?hc^ehczZ$IJLwIR8uhZwdVW4s1ox{OG+hhE5AY35Cy zJ4P`vSoxs@goJNo(4s92zWGau3mAYTF9>V7?Yk5@MJY06=8-4d@Nv_FkV1k3l zS(AgClv;&|h3!nOA(1_Np4k`}vMjKr+Sz9FpD#y~32CQC!fL6Lt4m(7m(bL0YG`gd)!~8!E0`fq;^7}FkV(xt92v?>xE}o4tARUu*Y5>6f+#ca zeOl9p@Vz7yxEDGRO^v?6J~=;5Z#12soH*T1X5W1g@xm{pnz9i@H50CxqpE$?`9OrlVxgtFCAR z0$aFi%!*1%)|M~zR)4mcrPm(_986V5!@a4_*v;fYgo{|W*i+?d1x01vDv$Xy9t@1s z20^~v_@Naf!LM6rOt*~@+sewyF^R>3h4l{CZ}&CXdWVMYHY-oT7ny1`tnO#Gc)FDHyXDXG-}gp!Iv*@pVJGkG8DiIVmSO^lPtFXqanAN9X70;TBGnDNL(cK@Tw{Md#h{Me`AKOxzqVSwGFU4w$ECXx zVw58#HAu~SD9L(Xl&KY?q9DB!SO6FQY)mo{X434gP>txFp0z?xfhi6Ow$5- z>q|b!RAZxjRVHY<;!=)@1xXZ=^>BD7hW8_o4%~=1GIJMuN<=lzkPFMQgLb5+CB%3x z&k)HmE`s#w4z>015r(cuND_&>?(%0e*sa_H4KG7fOVQHPWF^yW1mPQoSEuJbUqOEs(&ol;>+^p&%VW^o(?dCg#T&->K&< z3NU>~j`&3g{`x~&EsaKqOc6?kwRtQ80y)HFBPPTxNK$uT&TIE9?I4-GKzm_Ei=m!k z^<;wxks1}n59U09-(RX&NS`d?_L49x00!&duhPs%8wtd)ZeuF13q2`mw3D%kiEfKC zqOkgc2Ai!|Wdp@RlEHp?-*BGN2$5+?Jgs=+5WnA)&=rBFbY+P2+5~dd;U0WUI~LS9c}7B ztI19dBy6ataFOkiYW|ZJOa(mkODH_k8er69At!f&kBjEkeB_`@FU>0&ZSbb=8-z=; zI^N8&bmwMg>5sCqm=-&7GNgpXa#_%+H;T&0ICcEFyYXj5%J{7bieFn&Pfkt(@3cC- zSkawoHKN7ow4433krm~{c^KYD-u)aQ$pwE=LL^Xu_A5v&NG*ZK34WM5Tcs~)==fP) zf|G4aDIc&3A;v$mP5Uvdfl&e^rJ7+=QRjx4UXuI7JS1}=jsizj&VM8-ppg7FgEMm#5T-&kOViV9_s-JZ?s>1)Ye5v1?Y5zTfs(%kobKvfLn+8U_h2XS zg?zPjW;ZeZ(k&=g9})RM;R}0wX;IdPdA#(9gd6HEpvY$Vh!7d2W-~FC61M_VSuD*P zkUkR2{#Q-$s{W;QvwIMz=A9H27ZPgOsYoGIbYk`?e2H}QL_1ZniCb4!Cc2=2YI7ev zUZNZalz;Qb+YmxekRA&y_R93(+9Bz7C_k)FNU#3+cZ~U8_A2_)yJK~;JlkN!T(wxa z`%lC*XXy2+Ht%aL5A1w-(-{Q9IOgJorM<>8@s+q zvUSznd0U$QtyO1T>KuD-AC7h*#|#ol8c!|E$ICQi#G93c7_ZuU7+G0E@axt~*VfiJ zk*Ml~MLI!%FdE)hBeSv`vSdCMR#tF^wm`jjp#m|ZqczpFnQc$u@74aw`igX<=r-XJ z#FweoRs~ntw;FshGrPMJ0$or_Y^-d{NMuspM>5z!z|c@wX}#MjL%T^s-FNrL`%(MH zcOO`SkzZAWhKB9~7fiJt&myP}UI4lQiEy{oyRyTG%8`*umf=)R=jrjFY%DCS93Hi@ z`KJBhwu?Yenw83CrL`~x1>D%u>u4a3&AN@P4#dK?!$r-&O|JcVnd%Xaigz?z>M*JL zc9buiSb&7pOm#tkP6mHsw#He~Oaysb_UbbUlVi(RG>+%+q6d4u`2y!5TB%OG)l{p< zI4}&|`*ML1`1zwj(9*o!Zm`LP^rv%5pu(d3{b(Uac{75fc7KlYFc1^g;&u4L4oHMp zFVa9U1306roHkp>HW~VPnc3N`XZE^Ephoj7IXXTo3^X+7&D~=p!61!w<2rD?y_1rj z2kvO+K|9aoafb@LZd57#U&FHc5L+}^T-3hbBY z#~7dsr^4X0NJV;+dpg#H5022gPQSiRu;#aT9A&0a<(nCn%g5Edv%ox(P{CrKNXyl}VEpIESp{(Q+vp7TcoMU3$l|* zSG%O`o|WlLjA*e`rr~3xrYV{j8&e37t^koGuq#DdmhyVq>b^t@ZUg6SNu|x^l^FWY zTmFmldC0|i#5X{iG5OUtJDX3{$Z*sFQ+H%Ug0hr!^o-7-15Go1GRNdNAfQWLdu*L| zi@4XrIp~?FzT+E+y)04~j6{z3kP&?NTLC!+3tKd+hUogI{e6AX6W!oVkCg6{y2fBD7$AKb4A!|~{~chZ z+M0;dNZ1c9lC$^(KfZ8vOpHQ(LT-_v7ZMw;zx^c1Ue8RZ(xS?RvtA zlMu=lI*SYQ+)k!4=v-V}+VjKdCM4Nemk)J17BTDtTsX6=i0d>2imt5v)|jAf_}PGV z0}aJ7mevkjmZw}?J3ncanOKZ# z#MYV__7j&=PrkEGh^6hHG3v*yek$Em_~CW&-pqz<3@C$0V{&4!u$gc8aM<(_siUZB zyhx`}r?f~;r(uuZw#qj(taV|kE!-O?f3v*0s%q=UKxj}Wn3xNC=4$9_DTg&A#m!UF zpgSXyTEy8Y%+{^~D%k7hKe^gYRcK)K=95c1r6>o7EgEuSVjE!A1G2DYX(0A2$n3TQ zDw-fUi3a=AM<_?TDLj>n9}y0@@5FM^w<*RFQ&XXam+p6Zw{wiqVCqmVbvqeIw6{?EM6NkL>P%B>&-ech|8MhTNgXY1}a@300f(;iw*H zTN%}@DD0cfRpMb^-~Kx?1}fzTli5E@eCBn%AcHi`cC&?Ck_xj=zAdra-_X8d_ugC( zIj^u6MVVA@HD>kbl3Wc?b1DHgt@jPI_2>jg&49V+QYxw#?O{hqLlMXM*Lmm`)#Ls0 z$>eHvv6v)CG@_Tua!@$CqM|5;Q5fg(=lc|LWn@1QjclXnt5L?0OeZCfU~px=L;t5q zhR0@IcEm!yTorT%UCIF2jb7L!kw@(|9L1tKsO2>YY5uTAhdX=URF&eX>as%IQ_Dgi#W z#xk`W@{Z3E)OL@dsO8*O$Ulj{#j}I#YSha17gqna=#bAj0D0AH#N2$j@fl)n1jV2{ z$1bW62i#AKO%98ojZ&Zd6ywo99OXatx5g{|aLQo^U{T$^g39hE;d}%)v9!?gbt~?L z#AF})aJ4-9uAXM%@0>L-oVvPSJ`D`Whg_=p9HT$W^8iO`A|gXuN?KBZc`+a9M!;&O zF3`F}hJ?2`elWWA{L}2@;M<|pCiNoX&ls~c=2BrAr)m|FFZ8x*T&5bysn*c_ZiXkh4xMC_=0B^C`O!?i`>H$If<;3ZJ z#&67(=j4D51K-<5`_uLy2eo1@?mw2fpWO~dO9SOn3|3Y^ex3K>Xq|8BAjbihNY&56 zsI0w@6h^&~siAW!vfI)}H?f-2l1PqgQy>gZL>tH;!?aCFP1K@HlMhp*-GV#*K#$UA zzezVdB4HVdt*WGEkEV`B^KHf~~*>kpN(byAa%ukU2T zy80)p$2GefJ{Dt?F5Z}kqVU8;J<7n9l#p1Py>_uNpU--3F~;aIrNgC%7=ns>9)V1p zy%~>Z0>$lp9VO1CJ9fv%;HFXdn(NJ7vUafCXt9g$U5Ct8AkkvqOIXiA{PfD*xo&cg zmcwh%_&U>OD5exl6j$hYJ&KI`aj_ABKhxYdcM3`PyPuHzE#+}DL7SYJqEaB$&bzIl zp`W*9Vgd@^7MElx=Nn@K90owrkn7NA4y<-Y9Pg-<^U}}MlQy-OE@$=wW85dkm2n0M zA-0=4XVND0iF}z#&2LBL^!z~iijfZArBbj&O!uQ37j{cnm+p?Q;$b0aGcEV@22WIn zb(r~2M}tC80BV2ki_OKO0sYZ>HUe459}X!uU;e6gx4e%g>%v%7YWb8235yZ*>bbZ% zCtKU-%&mh|=tfgF1Ot(Ja_iPfh`@kOMU))WK*0Gn}|JBNO-{|Fi%6(iE65~fA zNiUDwLH2j4IOCmw)s4JtOM~JZ$|BOX%=71xc@*9Kn=)xkS#1WQJlyVY-n=~yYc>iw zUiaon@$4pGr7#9gUX{N@a{PdEh9s@Y$3Z>I4&1iiFV%vSJg^73#oNF{MlTmQ(;FoeX5z*Q;J*NkmI9 zoWx${>oqiz527Vf2_#itvwi%dA#prQt0AxW+B`kx5tda4At! zQ^Sb8!pdBXVQIWn5i>QtxFIjr@VG5{a&w5K`_D7v#8R0@C?0dp10m6kof5 zj-;DO;W88N(;?*ZV8<;p_|Z4dHkpG7Md;$SYO!K3Bci#clOw$bnOvn8qxDAIvh~<$ zrQln%F=M_Ub*iUt91JQjLssOsB1 z`sDF^Z|~1+;(?OcPY<|leTC_1pK@3z=bMuUhVk6T#oynsQ=u}ZFrQB+(%SI0z(T1U z#;QQ@op!Aq2b6=0Ygu)7`TlTpLeq0+oh)7PrjTZozd<0A(TpV(PQYv>J469DxXJ)4wQ zR3rooTA5eibSJ3#n`^nv5P~K#S02|N@JDz{0nf*YSL5__gIP6Bb=|;|cSL)Ar#9Kn zgGJY5Q}(3TsGRQ~eHUS1c%Ml7cq==yuNXY~nF=cv4{*hGxaSeNdW$g+H1Q6~Ju79r z9>>fwaIJBt0?_W)kqX_p=qEbG0XYAGE&N}$Q(?P9Roz+jUPp4zQn^Q<>Fn=H3qC6Hix*ZoKM)p{e!ACDP7| ztgl&YhA-J$BXx~r_Y4PyCecBEB-E4=#JTLwr9l#C-aKrl2;D%m8G>83=_YSCA&ZvK zRNlZ+3wE1HvTb`t5lGb*M!X}2ivKX->@_mU))Tw4H^c2z78%(Yg4j%#HUj)F?%yk; z8xiq%_@^d0Z~gWM56akn?(Qtfba+;luOwc!Ffbi!#MFZV>NnnL@rEzTF=0mmD(I;B?tpZmV186OX&oceI?^X&-cWlDpu6k)pm+s#D9- zT;w0R9%6j9w-5xG&d@1sbGEwiFT@F^R`xVdkBsO2brsw1kt@hIIEx z2();)F^0ZC>Uyo>0BsStxO@^vZJ^wk<##n@&Vh%A_pLqi?(Q264b8e0oiqWyZJV&W zdqBCaAXhX?OirUNI`-nqv;Wv7mzsenT0CLr0hU4X{Z|zIzwGth@1^9l$k#aOfT+o; zsZoyH(iAf`G9o93tUSD*rF-c}%+TqE@6uC;3oh;qZ*oUO+}zwwL9PnzjMH^#E%+P4 z|3q(=LG3+~4`l(OqdJf-jM#hcKR;19_O1kDaWRJMD1Z6g(YdDE%Wb_#0t zaCGnA^|gNd)js5Y@hi=pdxlW(_%#6zWOJd@=hqTImmI3?L5);O#iJof`6MJv|0%$|%WP^&GHa}2F}P?$x>QtDt*T;* z-Uc1z4&K}PmRe0;Ci+0G^(B8Ks^b3vHd?P9S=|P%DdZq5`1iNjN<7;hqjna zk3|&%7o}2;Lkd!nTL6*9vHxB5n6#eXJlW6|YUYM-;-e0ZOGskVYt@pIeH1oEGJ^S6 zcjIksOSv3y*+#K&Sd5Kr2O5QKV+L@mR;(fg`w=x`(CI=Y^QyspMKDFjV4(FS@1ePU z_UgYxeEMDo)YM2GOTCyS?uw87FOj-l({JA-P9qX?Bn<6+%)MaCy9hP^HALY74&o%GtmTZXIG`7I<65m`=zoJ z-n>K06~JO_nhsjRiVBPV@>{4De}pt$;Do-Zs1FxtD+Y_hcB|ezzTdTDwmXv-bxqwz zO?HaCOyRhbyJI3g0hsy_--=YUp3UseS3_ivbot-%r*xiq?PJ$$+&#~I(HC8YQcSCBxJF^ult}b+% z5DJcnt?RVPtVa{zgj;DfdTxSEZEn%M%2`aTJ0!krya1qHb0viH1wt5wtgNij zkUkI%Ii%Y{fv4(rvD8Y46oUnW!K!gezFMI)pTsDk;p5@p4A-agHSC<niAw^s-VTMkpw?7_3h5BL-h+_$fL@r`m`{<9hS@pX7;7>?w9nH}im5dgEtM;zzGX560hrq6v|t zqN;JqPR49GN_29xeqEZRTzJAbTW=>uEe#E^EE19vN1;_!Z+I-iiu!L7z>smzZ5tYW z152biLqYYSzHH~<{Fo9wpeZl^=RQzHS()kY)Wq0r>jbu?72P&JSZqsQzYmkKzre*C zB>s{VYk&?2c@|Q>7XFHEMqM3ra4TWFY2~ZXl><2ycY5W-sh8(a6fVk)81``!bh*Qn zlhAs@G!lPfhoPX&OC+g?v*de2 zU5m3br+isLm5Zh6IfFC+Pn{nKG|BH+egmvoPdFZTg^gTmPxp*3TiHCILR{*35X4o1 zv9#P3-6w3xu$`>vZuIgU^+@DDB2$z=h!gS;5ARLAgRkV$@^JWwzjhw-jnzoh@N;M*R(cYD7rF<7{Jt$Unc$|^pd^yo z0caAXhTg_fq|!K*jRBipiK`xYkqFdt|9s|5#?7LbyS8{&f7$bu@DUre(v_c$;6;zb zwk%Eg2_6RzEzhDxT`cW%o%N)A-dM-psknW4***Xr%I5DtH%~&U041ziwP$khWqOl@ zuFhqoGFcjn@!tR<2*}scY4Q$tQrsb8g@;>t{FHCDOkBF&J_dZpDusemsx;k@wY@ zSdcj;HI>h0nP$>@Jj1%jxwQ2vCSjU$I}rKm&RTgU;_e-S5y2%6vjOX1yl8)~H#mDH5!a-=bL_{ntqrJ3;b*OT z%GHbvtRtW`sj0FKfzsAx@mspg)ZAMEo58?P9V+GD;j&O}k`JnD8lh+QkWFnVrLpe5 z7Q5S|c$ z+`0E=d+{XZVophmszZnKvfk(*sG|9vdjR5)r^rC!xlJ{89!9Imkhpr@-6^O$JT#dk z!)8SP2NZ$+H#;z=eUE_C%b`qpB&|>JHXziXaH_9kK<&iDd~2)Fr7W$_W1I6;OjLS3 z-C!5n7n)kd6~i$HBjBWSi0bN6M&;kHc@-1BJ(+j8Im5L6wTXk?f{ryxN0{3-+9Qg5 zIP|pS97#TBYuh{RF;-l&*7InZH^)z~ujZmO*1mU?!M_MEzmB}2W+YH$%<>~vCgJwd z;obhzUUrkqibBdS#%{`>Lws>ks|kuryU>WB8Vw&!0<7a@_qWg-To1Cu%*?n<+^h4g(FGdoy^**XLTTu5Z28(>Sg8Z!6v57cdgOX8U(v0U4`WQO!(kf%OL8ZfUwP z7A?OoXEveXLKE>jKWlbI-FA_bip;VrK>OsxKEyF>R#lRgIqBRgwDi_?&>EPRCD zX`v`ac?3)^lckq#^dj604p$xMKr52+1V#Qki2XU)$7t21lYR5*)j&P%G>-GJ)s<)h zv&(bo-Us{$({1~|_z@eM3Yvq`4#o>qJ3FUMqzv_-pdbSjt3Q_^Qx~vjqg0`F`*VT# zk8~KPKLmi|<8lZ&&(_ky;Fj!*XORyAC0$sDgMnXfG&H65b-kr}_e%!ospqgeK1Ydk zv$;GYC3)M~(Td>oKX&VNdbeHT$^Zh)XH@STcFPFY1I<4mw4ah3lGQin1B6*7@hOFp zcp&$bhOB(?4yVmWMA>m~$ZJLN7AT2;=t_g~woj#oQO>}bgtJ}l$=je&=RKaJ*jQ1< zc~EKfGUHjX(&?$n7Z5~!%j`leBWuqYWy4`JlM#^$h{SCGnx+_T=mOqGowAK~bm*819E)}1|{qYv`0n0bWa5|ZO+4Kcu|X5haJCUljG;mftKKlJPb zl0OUa@z>~bs8|nQ|0D5ANg9C0SBM=?uUlBp}rpvH3eL_pLnw?-BiTL$m6fn4iU{3p;Yu$0KALO+Gk#M1sGzPpT`qHyU|I7GC9uGW5$(&+Zj>yx-uu}g5k1!rLAEe2z}Oip1vD0+U?hjb z3@85u0R4{p&Ss6*KQFjJH&H-ax=??_$VjZ2gFOXTi7xa{&fezYOKl=6XW7}*YYg4y zCJ#5r*Ulp4vR{t`EA>q?4$p6_Ta%OOwug-86nUZUcNOqFS)$pBCF#6jjV6GmT=5U+ zo`prHu<&FIA6_1?|4XgYJ3NvAlWK{O?vTY)NfV=se=TYwih)qYe|Jb9e*D6_q{KvQ zd~zLFPH-w0;!r|v_z2@gP;gLgmQ7<-xAN_}_m|IFE#bgv0HN2+b6iG?&dtfWx668)Qasz&iSHU2OUWDF2ZSLbH*hY@pmJ9p3JKG41I(WAK@9qjoR zujUWL9%K$T{)^?1i@k4;2iu-$Z!kL8bo)AcFz$NgzD6)RpAfP~yDpSSj;`wj=zolj z$o1Hoc))EY&`2RXYUM~xj20Op=^LLm!U?csd^JU-!9vth%LUnhF$z7=hVCpn8pe4q z(k$;%V9CulJ7zrIkBTLLq2SsK{1H;>4yW6y2H|}HxjMns;6xd|ZvR{Aa1Aor_M6YE z{W6vFHLAWoRb&*D%%W;6L+`DhW3@NEKbed}i!X~!oLkIMfUZYPw4TmyLQ!Pt&Q#04 zBx)0iKjq^fCH4C$Ul|C$_3Xb|Z(}+WW+rb4W<WAXfw{wDp$yTf3fIs&)zeXA9-h-?ueua@0}M zN=DwR7fzb@CulG7#6V$3&x?IhXwW&h3&H?tC*Pcu-3b)Emol2pB#z$uvSF-*Q%xG3 z-yn{W-XHcOS`~2dcBaY>{yfS6szM~B7uZUSTF}$2ZQ6tT1E8X%JKMm<#^f>Cej4X) z1QZq7#DQ%67B#Q2 znfPRkiQNIHW<0Dn*4AIn5kpo+byrtUM!cHi!qDN4nrx+v2gevI^}xCmbUs%t%@bWE zM(FcT9yjm8^l zwJaUHhF{1iiQEPv+^#yDfch2$7J=*ZL=FalmrQvqg7&r;Hipzgez2PkYd7AGcJa~i zDoQJL_Knt91ggF@rm!5FGa^{Uj4HUD88nz&aoRCAM>7sJlKYGubZ~n*0GpWa;rKf@ zkmDYU3J~lmd9~;TF7hFd@HB;6Ha^nM))u41MIO6F%gxqo4Gj%#?wXG*A0uEfacM*B zBjNWTohY%i^7y%lyW-2fH`LP3TH1(^?tIGZT=LtAXE>Mk+%k^sHiHK-)Fu?Q?tsAv>dRgcR3(FM7!EHl+QRnIvE)o z0jAB`F;uU_ zHLgfW0b%`U4|wPjib9fBgFc$kVDLFRK=&sYa=}|} z2o&IEs{x8DIWj=Z0yveNJ`3Q#@wQACSb|A&NI{j2b(kT+K>e$D0)jb7@|Qer4@gm4 zfrQ+HgRvQL1X~cvH&=5>;WS6O|J@dzmYpLlm-)EjTbi|{R>#W3>|?*qJ8n;7fKH~Y zt+g2Kx+w0HD)xGhFD+mVZlf~-sI2NtZ4rAS%7uti2;IHQGk=+en)+>wdr)f5lGone zL`lhBh+0>)577T^-~Fm|P&Oj@+Z+;^$&d2xiGwFF0(#4vPKyV-n41@YROGzxQhe)u z_)4*nkMYQ*+4JNOKQ&bclez5vNiGmn2pu%^+T6eHyuNv)LOVM)J6&VXAgb;z@w`Et z@;FAL9MS48aGc)5xaGYZY8T^Pm|3i3eg|hBcRlY5L(fQ%rjA}X0^^0cP}0H?aE>?H z*siUS)39=w$@h2nrCfW%g+V!AreGV;&%ndkYjiP)tmqLg3m7Rn@>2FzTY2x&FZLPjWC7Vm(z-6xCVbYPG{^TaeB} zm#;hxL|h`9+BgE#7JiJDv%+k&b@Ba^EHCXlXoQJEQjUFXOUn%b2IIE(PG*uLNJzMK zwfXxN@{_~Ijg4ab%z}X=z@mc}I8<@j#(jf9bYW))fn(%LV zY_3kg_r=pv?H@jNL3!Ny<|C_Utj#Njze(fy07~@e|fV|@HS0EN1pTORijHQU49w0!_x6A1v+>< zy(5lm52!cB&pIy58_Cl}%9#VH<(+h%;;Eg#i(N^H7D#EV@4kYC1BAy7{VywGf7GXi z3)VeSEqC{?C0kfz2X4&23Z@>%gI!8KhjoLU#s&?t`$AU78Up^Y=;w_AO__d+Jy}z) z)WyWz7vkYzoF>qVXO10t)@ao^_rakgGAa~x(0Lvi{jLb}G$Okr&(dRQAk3#Q;cEAH z+mrQWd}$SfEM2v41)mQNNum~evj!7vudg^(*jTzQEQ!@_q+B+#4)kH6LEjO%Of4+D zpFUro+V^~ovl{SQ4jN;&YXg!)7mZ`-ms^w@q>rd1s=qqEfGIvs;mNE zVObBR9&Fy2i#=l#6RFsLiu955vS&qX6|r<9IZq*xt0D&oot<8!7&yfq%H(8Soz|gO zQ&$a_b84}#KnZh6%d_G^$yw$@}j6^&S=x7#IlLn}oR)7|FO+ z1@gJcLR{vxl&#v4&l>*zj<1C9(izs))Kc#ZO;PTe>%@y0ChMp`dTyJEik4eI zjg+~rUjKoW*(hdXelcJJUWe2G$|kp`cq=Gld)kKP)q#B9kqnQB3m!T?80d~$=*=Uc zi6G6hTja};30}3&(SFdix4X9p3ia+RB}3Q)05cfI_LrW#LyFV^DAs@bLru+d-h-cg z%nfaIoiD7?FLqh!J-x#5_h+9RAOF~3Aq_m2nx%VV<3VA;SLul5Y(w2%LH4BK`^ZRo z4}}cvHm&>wE4-m@g$~EjQ$D`@1=3O#&)t8G2i*i=u%~pibh)3m-$jB8si#e-F-|?X zn#;o>L&OX*v6Y=nDADPWEQx{Yr8hT38{<4Bqm>7$Lm7=Y^jhK#FZKc=M;P1@gr;`Q?rNmq)e*6VdK_yM4^Ls_xw$?OkYR8t5Svl@;37Zwl{O;I2A z!@($Hq7hB(uxUnrhgv_3b2WpDyE{`cWVNvPags8Z?K)CKzph99L`E8qhm7#!<>m-@ zY7d{P8-E5aK~+6cRx+&fo-hLDg3KEdjA4#;gIS_fOQzYhft^aZL@xd$%br_{)uKM> zCeKD}&!fwvfxL;h?j_HyTYee#u%g#K4caS`Qv0v5QXo@$`&{6Q+*6VOTQNf@iC3Gk z1qC-0+r>^is=UunDfFVedm>_zPD@@U#X>Ya?AmfXA4bJ8EM2j31N_zVn$XW#R=FLh z{3Y(O)w8xehtUF`Gma&Aj1@A!dtoLkKjBMAOyo6MqwG8)T`&SmQA&yP;gX`*CgfMX z>G~?r@L%kciz6#*fnZXN^>}9gQ3KKiE{Ky5n5gE$RiH5E7^&Y`6DIW8r=zDQR4aLj z1}mC&92rR8+1JyET_AGkNL{W^yTz^;@>4t`kqsK$A{x&xl`a@%cxVLrjYvbltUCSo zPw^$>x5mCwK7V%3fm`~X-sPH;-E$XFBp3hONvB3HSC>5fe=cAbmU<;8oY>Ox z=&~*Q%IMVmG9(yMl7-2ndL~t7D~_Co@oD{nD3IRM)9V;86#_XaKwHgupJl%xU7XM5 zwB@?@mW;d*X0S6$xo>ACMXywQ`M3AQzExgbQvea>#~8%K%b^_@%sy`>CR79RzJACq znqJi$T$q0=zWU%Hhf8ZH0YNJXX+?VEh1oEsx;j**Vm9>2MX%l(OsI0b!rtAj&?+GbvTlKM z+RFCrwnN8>_Zdcmf)Bm9=Q)flRw16js)`WLI26e+IK*ZsGgQ~;>*OM0DvhL)1>;+7leOl&1^OQ}=*)PzrMc zkIAb4_VOTG*h&IX{b`|k9)v?R_OQ~xI&;Qvlh1y;%(Q%mJh84J;pk>d#Lv?&gAd1| zA|fC&B1GZEoTq0jXRCF5nlm|@*bxJ#pFa4Bb z?m?zXQ~WwTMZb4l0`x_HI|pS*l%SSPRJNnH&T2bxzB~RHSh&)-FVxlizTVzR2P}1- zo#|oGr8YsRk(H_<3dB?Z5MLcUd5>A=d-Hl=G#HQ=?Ye0`g{gv*hR0(Z5%nRK3H%!} zTenE(<`)-dDrFT~dHT2gylfVhj~v4Q0D|V8&#S7%M)G1}^v16H!B!hB!baU0veP}2 zQ3^!*N6`8aIzsaw(sacQ2J}BBMVOL0kHo}$EiL~wme_LQcaG<_vAhlhPs3??YJE(w=(^ET>d@ucy@p)R4*w@+{&z=T>Hi zz%n`A_TSx)n>=G1nzVhLW;B<{=3W`2?nbZ(@bH6^5fa*V?(pgvy{Rx7XLGT5`R<&t zN+ijG+(oCM4)lq|ABulo~_lfbh zWbtWwrGcW>n6aqfVBXgYP(f8@>5)PIkn4RG5Vwl|wly}PO{npuSjyuqkJKPQWhR(C=FM`FyeNd%Ze3+X=+Y7Dxv98(%rVD)y|hs(=4 zEO+3}jpMJR|HXt|X^sea!ZHkQM^=Lpb!?0ib>=>psbXMFGlR7P9x@W_(UwhN)syW& zlg7%yFl~gp(68A?n7gc2uwF@$b%(C3?~l>2dd39d9=k^-BRdYN`|}`BoE~w4jCp@j zepYVoP(<>E#XvrnhgQZIApq~~$5bokh|FHYMs6^&fI*@jzI94oD<9Hn)2xTS`;|{k@y9r3%9#s%;Xgs@`X5Hv|QLfp9gZ zWHdQzJ$TsLeoMgiSdQ;=1e=*m>ZZ9V*D2a#zTI7Ne&-KG>BB>UEb1k9frGj_nX+k6 zp_r~=3E)}h*_qZz;@jlb#}n*3GTPeq*y5rY{poAhg6gDVe{q+Xsz_1p|4l2s5|4dS zPqbpv9nXaS^WpcwguuW=_iT1d*-5X_urDguIVKjpUy|!q=axIgQtR#XhJ&ShG@ss} zG6XDg0y+jAr($>=$}*CK1j10KX`9ksSs!-qRU9e!j=SfkmPn&@*x1=&eirk*tZuiy z{Ij_-{eDN9CF{ztV-1Cz7$5SLR|`#iV&cSqGCs=|z_(?+M>wuJ)uFI1> z`j{$r74>|rm|EU}7~KhfcNg#5&7#M@^hi#hivRfTpzYx2baWE80-7;-I4Uxd+hnU- z-Tve?(LCA4`dJ>^!cFb@wBS&E=xtkuUX+DO$CIlE1k10KU53-{b-x2K@3A|`gP#xc zm$aK}l>`rhmKWht_u*T}q0ef>XHNdz^W8ZsPc6GoMMy41vl@Snj@aH0v?ZZ^@3MBW zur)?N+Dr3{yI^UYklu`4HmP>`cDrABfEv5;vm&keY@lUn+CB#irlqIf*b*m{S$b<6 zW;9f$I^rT9)INRVqIH*3{nP#di&HX(*_dxJ=F(7d7H?$^mi^xy@)W#IKPlv%^X0on z<>U}zFHcNdf7sekI=miwxw+ruhkfj-s`R9%?YuBqcQc4Svb}EN|5Vy=H;Nq@+NsEyDyqnT>(J}|I@SL<)Xy`+%=?zBp`iERCI6ZA88m}q~xkr;k3;w+S^hz<@Zt1;_?JMbCVeKBq_Dq=Xskxa+85wu3?R=GF{8r3n ztWt8GOjNYcVX_Pt4>-NB-eh~BM~^B=$M6=(o4rNq-CtvHdIA*kBa}0*5C2!nda$sumNcGo2rFjv3$l(KPC$2k^XYQTTreAN1;od(C z8L~-oGuB<{EG0&$bENlGDH|BAx#J&oupjiymEcaQ|L}e{4;>>b~N6>bT^+Z8o zSs4rk#Z1ytQd(AV?MnFSi$HsP6TdDW3-SC0N8s#zacPO#T9M0zXv2!ym4f5cso=)} zaopm}Z89>Y_5&G`P7BmI<|7r|BuR$J_<&fzt_ z!*7fJw;C1F3a)UJe*YMUuwD#nXlqM(1o;X$zM>}+$5$|a@)HNXqOEPFMWUJ51;v(8 z+o?`(8n3)-<<9T9bLS4J0KSHpp<#Gra7SnN815zfg$aRQ`EI^x@WshUN3!g3CXFAc zM75)`?&9HTAD4pTFOUh#0*X`waw5xyXnK{ken(7Obb+Q6>77a!%KlqcCVnFPuvXPr{;#v^ZYKtCAo>Cm*ubPl;QGts0l+H_qx?9ji%k?m}S45?8{1) z9{M|tIF3QB#250G^U`ZLhs9ai#~0<9iRFK0;4uC77RjjwyZd;- ztaM2U32jG4X&}G+zRL)Id9VZwWH4+3<{JIKN0_(9I!{~&Z>>~WOwYJa8su&*b?%fDw{Dt&rbes0l3}iNI=cn@xqDvg7rgC{ z9Y~4qK$E#zy5L?JduEdc&3#tZ#q7QVBJ>9mlkDXt^_fbakEtc*CQgDqO@u~JIQkdP zrVo+do^s|&N;k0LvNs-pwQlqnXoC*aqJ0q?+hCDV%MsAl>qOLI;_ddvx(Y4D2(OFC z71(Ii*t6RQT|QJ_R5BSqabDp;RJqJ{Bt}docP)ov@ME;gF0E8?h9 zX6E%Z!DV^D|IVpD;l2-OT+1xB{lTrzd;#sz)xp$*5^AnEQZ^fwZf}ThVseC2R%)5% z-HQAd7EnJLj09UhPq7*49a5M1x=JGSX=QT+u62xTa=SVJdQz;X8roVNcz0DTN;>_}@2$&gKle%ui6 zl|RmeCIT;ntlGu2Cwfa)j!EXdnYF4c9T!oV@)>11!pJwOlbxN8{(j=$ZAa(#GC9FX z0N^opsn`F&1z%bt-jSdNfbOhW5Zabec9!t)=T)OC6onn5N@6MJC{rajJtxBarQ;nZ zJI@C?3YEc4ujgLLzl-@liHENd5k+93%wUmI2XEJXmPj*zRAc!~3$y*Gaft}+8Wntx z4ylS16@t_EZ*6iPhh*aOkKyl#@>~FX=@#QrR)B0r_udowl=u=PEH#jy(a#4?TUwq@ zzca=MN|H}4t^5MSU+PjNy_u*n2fh!%3t~}{Hv`vyQ6=V#AT1VSyUI|kdzbC}9d{k0^L8k0S~eVLBlqSP%xnt!M3lzj;-0nmV8Pr%)&Pv>#e`PF*nsfG&k|)s4w>t<`-0TnM z$5N(#R1*`ouHJg0=PTYtHj=mcm!ABJ;~j1$ChX2qr;@?2rO^$xRo==y|J*Y;7 zznw@xi&q1H6@XO0FvvesP*&EtXeb-K+>y?Rrws%He}(kQfuW&M=MeZIlHWCnF65i4 zv+>y;bK|W8n7=HihKE>3cUhO=^0!c0Mrj#oz7!i?2j$pc8+cV;+OD;}7p*xwK*mHB zaq`SO+$U?&faz$5otigogzT2Yi3lTU-z1L^wVu^k1bIZYXW)ff<5#)xRzb2^Zf}GO zc>#nm-b?6UJDzjwxa|PUHOArvV8oG|$HF^y>)LcS?!j8uIe9M=_=mrBDoG1hp^uT% zVC7_NwuitgX-fWJ*O#JS&0t1TsPX&ct1l3Ao${0P$++|JDf&pcN-h|gI(20%ZsE;M zNykm42S&<{@x12SJ#P`{eFb>ONuHh_#q{v~YR)fi%(bIgS&xvv!0B~(N^Fm?SSfh5 z)ZBV)*P(2+rwF0FGX3DhCiS(bG>GE3Tqbp_LP8!c8KAJU}`%09GE{nia^9 zlO|>VQoPb{-wjbih-ATSh#&#>?0p2$882K)`Wl8-pwsy_)i1$1U48v!ek5!w2E(~S zvjGX%wZ5;cqeZVQkjV4Ywo>_Jg1}Y;22J&A3{{x-7n($vUqXW#Tj1U}aZD6v1_P!& zWdS#L`<~n)X%!`??2J)pv}=hWsKAbi(sK||_RC8vM~yr{jX)I9^%s|UEtc*aQGKu6 z&X8-2J$qUR$CU4yi3En6+hsquVY{_OO`xt2Yod{>PmMn$r$(xP)c9_+*uE5$K%c+$ z%XiqiV`F+_1_RGjnry0m$bvsf+J0t24jQ?+@_n~PFcy>#4Q(Hzp1ck>iZzbSqr*QR zI6(L$aPNC(noX;7ln@Pbr}L^Ax$ew{+;V6oGUH`UMwP_wiyKax+ObQh&j<1beZUjK z0y$GdQ|Zy^?9R>pR(Xs6^pUxM$Ln+2AO87;6=sr&#qp2yQX0X<5F1Hv)bX7HuYQ&p zlLjfGeC!!YZ`7g9(8Vh55gt-hiE`58qcCy)&}?pvd6Hh&K4`gh5DKlWoZC&oA6udK zZTI}|&x0fu^s%D1*DJIJNqRgX9ZA)|&aT*?Iw%|^s~8@0*q8M>rb6A%?^4<=r^&XY zwq^A-^VJ>abyeha(@cTUa}dx%$`IWoE}M26DrGwm|HWWf+`A5g%IZTJ8Ut;uJNVFI z{cE-s-hsi4MbHi>1~1B( zA+NGNvCOyA@j$x;DkXgr_nXA`D2*dL%7|Eq6SS2!Qy2gGPrkJK^W4Y#5vu^c!Y z6XbQAzX4HuR8cQesHcSWHq?3;svtQM5_gI+dCq%y7La_U37}{|%f56xJS>8#0Q?Zi zta(Kqw~xY8nY7v)TD~I}DHqaT8T|V24HwaLYK;me6fj}6&-eme@TPe{&dM}0YO2iM zsH<3*s!1!0Hd9{Ac%{38zf{_%N_<8__j3&7UjpZ>ULZCU>Nl6*w1UKAxp=cbtb~-a zf_YkvAhJDL6PhvJdhM5S2Y!w7k|afZ(fVwa-G|SALw!D`J(f2SoCrVwbnn&uisDx1 zG=3_*kovg0FN)paoUn!1NN_gYww+mDV)j&q)rLn-oW;Zv3EW+bCsxd?(3wFn5UpE$ z9;JokhnU2+@^e-jslgc6xOhs7?lJ!41NQEo6yMtt+8q9Im16uIpFUJ3GiTucd~h@D zOAEk8QP{eHw{c_wWpmwI{W%0MCNQ6s<5boin5=jtV%a7dTK)V$rDQ? zlH26ruM${58d_Sq`?~0KMVXs&*&eQbuB)Z_ecq=T4-uA8Yk9%@JhIH=+yLcu_=+g5 zrY9!8G1N~?NojJX*$?ye^=JRxl`!0LBnRBa^hguw-0uL1Z!&6me_N~8Fjt&DQ%Tsu zZ*%IDz&{1n|DXSP>kE<)*o9D# zmoF+Re%t#^3Jf?XuiF2}=;8O0*0jN{nT=)`PVTesNfC6_w`n+R_A~A_aNYKG{)KCv@!+MEG5^{2eE}Za7 z(l{)bgYC;oa{JtW5)~_Ue~1cie(Vdv<*HY0#PEAvsqU+1{({olAAIqoMC$Sj9jaeH z;(yEE%&A&mL%YnrHp+Mlpdw23a>gwC84{jvv`)q{+IH5_OYLqYiySSe>6+WA(jJmwMyH7m~dVs$)jP&~Kn`7*8AorL! zY=oWZC5~Z)JRG@dMK(F^nhw1}dSlD3E3^1c|DAgPbFjhR3&Lk&U=nWED$5rs5V74bQv^8B>AbMB3?K@4sVe zZsfZb3_o^WA0&^Ee5mIOX0!Xwgt$Zf_+G-eQ@Lmo(kV*Nw30iHpF1g02Nx5wMEXC+ z#2BflhD*jg1{1v;{P5S~Uv12F)7sG3tbgS zNH|dAb(j|gH*(7zR%I1pZ|}nozncUl&`#=Q`(!uc*2ZMpUaNrbuE2^5FC|~kSJj;w zqww{UqMfNV&F({LYUyz9u%Z$(4FHrx3GZf3;cFN7yX_G%>d9py^@=G5R?gA@8&5#W zp>z0{!}~t%gTo(ro7WAPAq2b)CMJ*z;=;Nbh!h>lZH(#%A(DZD%dF13>n3)Ie?z_G z`0zdvI0%(mG_@N4F|3f?CUM4r$o~pEu~ylAVI)Dysj(mY^psYkcF#QqhR;C~hGQ-b z-%eM-^CBg7TxeN8Dn2XKQeq;(qs?c&0UNG8Bo|LF2;|aULtz|uea%RgP4y*i<)o$k zfXDYoSh2ZAN2T>a>1QR`-YZHy zo)frr_tS#V~*81i<2Nk3i>tgXuxHOL#69J};OCqIZc)uB>>$ zG65_9yY$k7U*c=Cns(_WiH!9(C+*NU{9 zoiUi9-pHsB@s|>$JPw45!^N*lA%D!`rOKuqqCe1}1&khK>}kz70C~(fjTyZqZt?aU zHVMpI5Q}UDfY0^W93^WTTBpayA0D19-=X-~ZuBkxai(S_Qp4#`DAj;iCEXR160<@P zTV9+qZonhs?o-1M)?(>6`8qz=z`(ebPwOVLcKZ%1`0>F;`d{%Ly?xren-+hYF9MnF zxt|I-xs#VVK0 zk@-z!-B=Yd@q*V!gFAK{P&QY2^2yo5?i&NYS@#vA3hcV`Np%=0FxiK*JA18*p>k1H zYw)hXLB!hY*>9D#0DwSHxw9;$Oc$gFKB(*gtf$*!vHNXjywW|M1Y#ajIAs@m!JXF_ zJ-yS^I@wZ72IgZ|GnGg3y?%a@@s3DaA!kiE7llRQMW&cX=2hv9vsRxyN)6FEca}^* z8s7o0I!bQDG^JD&uZY0CS*Ke`ot~g>zQba~;yAap>##}@b8%1_M7+tjFRC~;d+Iec ziFLo5-HY93GgArUizMsLY^6XS4GO2n*D&l=hp{X|!lC^Vk64us1IG&K?h9pgbRF|@T_VK-ikXQ)QXs({j_tJwkO{kiaZtptYBBawBhIPIh*zjed5r{c(0fx| zL5g4IEAyy=OpgRT!33>12m(8)$ja!QyP7qCSV%fBYyx~D{ZbVmbb2SSV_`pT+5)WUU@T274p$;Hs_?PNGRzMw>>TvUS=I_`3p8{k7gwdaF3TE21 zGUJC=LJvRfgM0THle1(2za!52bH7VxyevdwiZQO_OmE+NdhYjbH$y;D0Lwzc+%^WrCEeeicX<2=@^qo$L0H|tNUns5`S z=#4p7Y@9ch%7X5j7t~#bs4*pQlL)jgaO&QHj-hO)l^SiZ0z717be^fugQjU+A~?H6 zdPi0|jlrYZc%T9LBaDB4q9P&!v2bZsuJJxTVh&`@ou221bHBvKjxXMnX-mFDZMdQg zJ<$FiS?Y06tj{C*0X5a|4!;8w#4umv;v_uxSC9<6I8Hsg@<*EYfAQxxe;kSa)oyYn z2_O->#r_QAdZppq0?;=c z>FM_y+2vWQj_8;*hn)}I7kaada8P3xAdo+`7eR1@XNvh|5-MxB-h(M#;+s1hj3G66 znqynwdG=)PKB9&B@w@plauLscSQo8M{Q=z8`~X?rhM1=W^au_jFJLwYh+I5Es;Q~Z z>_q4?35jKC-=aGuL@>?}t_+z%;{xX@!e}V}YOMyxQz|O!U&Tk=;oNqg+|JC{x!w8$ zx)XCja*9pRJg@6C2SzzvH#bSshZOBHQF>)rjf3NNmuXKSIzT%<*YygeQ$Q>lMEIls z*3WDlGvUy?2$s5wxq7q#wMx5l(A4v5Ir}DgeySwIo2MSE#0=UPj=IT_3Xf157t8zjf%k}N zm%q53(gH4RpAd!I7!EU9JZ)qM{BMviqHQeA2fq<+uVN}pHskOiAf2qU#1RJc{sOq! z<}rXlfY`C&LnWx$Mi0(`PWfm*#%ns85grRc9YH}VSb^xx50w0<$NP1D`B#;$3NTxa zFOVB;&5wLt)m@am!{f-#z;KYL%Me8l{zk40EuuM$N8HY-AFpyaVWO<^xTd9~q(tqt zQDtu?ZWw6vrJI8NJ12GVT7axcOCZw?+BvgH>~e z12jdvn)Q>_H5tvgz&_$|aAPA2^5%US0M~}@ywq-S1R%p++b1!kz=ijRE1OZ+tg3Y6 z7<+}%H%)NjT(u*ZLF>`UpG7=d^2#7piv1>z(%%31EX0sLqQ0h-O z!a;GV(5O*IqBzt4Ll1w!gxHBJ#wk^}tmd_ZgpNS2>trAi{ca&UKsV_7 zIJ23IsLH-KTqzJSA9BC}>fD@Rr)Pfr$xW4)oL|5F-?i|SI5}>z3Bs`Z*pg$9e`8GXLL?)-fLP`{VtWwcQ|4em=&C%$=&RbJl2|4kNOB49pK9y@*W zZY<}WYovLzV{{Rc3T}FS@n0S@+3zssZeb+^pTe~h;+UzNez)LyN^oe(KjL>?V6I^3 z0hSC@K6Kl1U*AC>6uy5$Z*6)FTaz(&P>I)*3&1 z@Mi)QN298p1<~;=QM-fm%RwU+HWJZWy+BQ@49VQD0yh?RdTGR|@{h<}tz#s`(0wp^ zmrRhBCdZ>sMos}x?1y;T^p~i13I6$PYJ2N`PPdlBMggw;MteEu?$%$;!`;blG23KB z=29#PcE^QJ4&Q%jNQ)bwK`6*7CH|ZRyA0nf3EvUt6S@e9Hj1_2{XmeG)iqFy7_Bxj z#jgXmMNmr@?E?>a-xSKOuAvf_Wh19lgVU_xo80z$H$ohNnZ(&X@BR8m<9^#7H11h6 z0{JWF{zi!7zN}^I+0v$4SdrnL9aO$MBZl8;VrgNe5eW)foKc0ET`=%@NJ#J;A0&BK z*{p%#EK6bU*TI7u`qOhG0JAF1p*qdSi`0ej@OsA#G-LwxZIuk0nkMWQ_l`H`&2G#* zgp|%L&{OO~F^Sct%jngv(5!(r3MKH{Vu78Wh;WS6&~Tc7752ONGb-~D>qHU2=?N%H z0SgwJf=KCJCR8m_LZW8#e!no2(*tbE$uZt+{3oGWoXKbOjo$0Q)ED*eu7C*YFoMvs zK{bW>ESsNRsA3WDS6670AU8At*x`3zg@(Y&5DBzKG90zagfk%K2oy)0Rg<(zmCl8wkd{ zA}aL4R_&qjZ!0YQ`(Cn<=1-A8@qshtBQyLDiurqhUF8=}^dg+%7xx)5llOI_BMOysP_9 z7JL1hxiCNf+K(dMG*!xZtg|rliCt+j2sWdeoGDMdVJaym#__vv`qyInY7i`G9S=F$ zx7B5y`}_E$$y6l%3kv|TwUVMNovw;l#!FREQGxN#8>%8A`nn1>^&44)j{|W7FZD@e>qxP1TDfkRd zJ$p>dRsG4^ugc~KSmZXCFtT1qN*0M8sgVz{b@kBI67ksY^%jKXc&uiK9t|$5v8U9* zp~Z|Ki;a5Yg(r~?UJeJlkP=%6=~t>jA{Ov>u37*5$&Yq8Xj}HfFJe&qma9^o6D@k1 zi>uVF&dXEy!NZ4n#ibG685#5-6bv3a=-vF4Az2QYzsWpwr?nx1tIeH;mF6-uXz0g) zlN1O-k5$G8yl(7g+yviRsJzof|AH^G*u~dp9DdpuO-)S=;cfOk;n*gHf`^xj&DGK& z>@|g-ic04or>f7SzYQDBS5~4GLwe`0sJm|FFW=VqTy~Wc(t?DbO;2ds&M#(jTqRrU ztUa1Rq{1Gjw`iB6&Rqa*9=~}^HbT*2Z#MR+{WfHVz|#wA-!Yzl!s>Uc)*#xH^{vD! znU^DySc<2%OM|O5;}{4(UXMW^f4H97<~Y6Kd3HDOuG?xa!b?R5Mma#NRLb3A*fALj z#oCpIM2KPZY(u_7P}*yS!S`l|)&r0Ja@Hg#ZNcGLCzzjl#vm$6Pjrq7mAroKT0kss zSoEQHP|a|V0h1IdXY}Q#Age1J5P$mqF!(TNC~deog=uSkoh%ON;7_uIv@?Bix$4K* zr-nbR;XZ-g-Q7G`iacDDHB&6#W4)gZ!ka0YyteNttv7ehqARU1N@x%x#-9E1cV^Me z-nT=EkXbZXV)Bzz+WG~kl%8D$J>~4}i|+f|@LJI~b>xWx(|vyNVpPfe{O!*6L`fpE zSkt}~F8sTwDQVlkE}y)_dhR0$2=-E)!0bS7nho3X?{CoF04xAC2=iq=qC|k7j8Nr5 z^FssBQ?}qDFec1(MHj(qB1P&ByUUgg8vniW#z_=}bSF77h<6FrsYHf3F7GgLFyR*Z z_le2Dp${<|JF5e(4?v7tq|RpAfQ^Ho=Ryrt!n^!9y*G7>f@1K++esOTKSh~Ls^Qap z_SGJHoCE=6--=dqM#Uep^0sR|hf%zFj2pV2VdXUj8g7h&F`N zOR(=d4u4(Fyjfz3@jZ)cNC@i5(2593RtF#C9^t#`g=$hb#}nuvKh$j+^rPVerxNt$ zPJ*Mg{2gofm+s;LX>dOV+4m@JH1h=w;aC)?NpqpWf}Onwu6HnHt5unVqGSEHlrpx> zMr^+XwZxR}G z8;`L?a+7=#NX65qoaP~8q?7edTJPuXi=#Mj86N!(cSrO7b$c+9br$gea-!CeP!X62 zdi&+&aw0&GUK>>TQS9SmkFpqBplb(^cHztt$+*XbFhf+{+4k~q)yR8&6J=_oY@PMv z^j`s`s-*zo{TpaHg`VlfxSqcp*T6qqu7l{E^Ps%@iFyT!bGnOkGOfeRVOz?=lm#Gt z{Vx5aO58nNSypVH^{U>P9}jPDdh=nsc3uR_%7UaL;5U*@!6^v&2K}=s{^W1KIf{D{ z1IjEHkH5$U?RhOKF{MqYJYjnzi1ht{j)mwfd>Pt=tmR>#Rm53?k_?y%Dw>PBZoEFMDAWwvzYwV3TJP!O!57;EQ zw*w6e!luaW4#(OcY~SjX$F<+Wl4&dAC9scf>0>jPl^KRs2xK0}(wxaS+JDW25~s zhIdnOV}rnhUs>xoYUeCL0|-CY)6>`Y5b@4^tWmpCeiK)xd}_DK^55}% z8&hOwxerS$O#jFc|KGQX;GhwBFP{@8BnbHuo}8GKm6+IaLBdIJ0tR5vQVHXEK=3{8 zeRQf;1K1VFM1@$Buk*m(7BQPot8}yt7n-2kk?RYs5Hbg%tpOo07Lze-?qo+oTcwNb zETD22I0|Ew1!H22?f8V_AMbcucNv5Wh>?n4V zk?&?B*^p|l@f|soQ_(ZVhxxvG^XhGcVyhdvXUMC#Z(DPs(n+c)EI2#^^Z_toy4J!N~TqE2gNGwggvp3q{7ldFQn9WtlA znWGC-`__BVYL#LU9T+HPDyv4S{1`S_@Esfw#ub+o(Hm>1>4PluQe|jh;5Doelo<)6 zhb;^*t8;{UuU0+RwP{IIRG*%n;>?atijN02=9zba4)()zCTF7!hmnl^WMr-0hLg#3 z^LFb{LQ={{57*#%?NVJSNT`Ku&SV}$C7;dQVD6Vn+r?p*MbBw?SHDl;Zi!$K6BRw& zOhKU#;DlKJ+!xG@CFjmYCCCC%<{>4x%`cIWk=37e@_R33R*=V1P*fbR z4$`E1M`f!c-)kCV;`U==t0wB-z1w?=aHPI%f_^*m9o)`got7W|9IM0|kB1kn{6M z4$Sd$u`bbW=Rv4tIXXt>HO-(tp)N9W6bE;~u@LaZ$@1nXpB+TG{RHLt^B#}Xii_6} zySw}1salXL1Nr2*Y@bosXmJU)MM|8 zja`Keh7SW~@%He?5;M05{NkRt^cUh815)`yIwp!EOTM8EgceICk6@hacyd&c8x;~_ zyWPuQRbUbv6IVqgUn>pMzcL9S6-U)I-vqfzhi39DuBb!xp`1J>M7?D{QuVzstrFf4VsMYh1@* z*A5cjIUs{EjaKT>){81=&&hHHsXXMM$||)*bl(gkbIJtld%i^gMiw`%HVIi-r(gsX z!X3Ot$(iLh*&{h~qE>m&C3T)3>SOA!urkA_DPA14Mv8d4zWR%Oskzxx5MP}EaKk1e zD8kU=&@A63!a=XnABw2;hkaxLL1<-DpRVQj@gd)M%+i@fPAI9#S*WjlzaRn>)roNK;BJCwkH zEE(}QkkW3m&0WxLV{m(ura2IgRb_#?$XJPNxay{{{Pb>hQoEP5j#VmmFq7$z)5Ony zH%D+GNZO5Pb)s2YX_@#wpcoi7&xC1=HHWf5R~m{gRerK7H5fYNSi{WZ?y@o{rwGI> zuu#pHl-O>bS4gUKnslMYS4YSB_Z8IM;Vw3WQmCi{4u#NI7Q3}6E(SWWW82-9@C4`Y zaRpY@3T2dB4yTtbAcPeLX%$yHyB+9d#-Tv$P^@Uv(7=56-D-lo!V$uDots~Dzwu%i zgbmBO`+ScXWbb1_P4DztV?P8Ha*|fO>wPz9GuO@z;Uug#yaTL{DxFE9-nA}qz4Hmo z*}Z&eK{LrnSc+xdp>MQ4J&42aPPh1ewp^eYP=`@7elW10k1}EKKFov(ac1c3!L~ve z{e$g=4u@Z%npw`PAA)QML$BVsR!0f~jm5WNfg=h63rSt={9nvp)Ys&DCVY5Dp(A5K zOU|7v-3SIq+b4~utuD~wO127qO)L02FVC$1n0I*fZ5NxN6BR8qb+~m`{IgZ3baJF# zZe4mQAyil1J;XreRhF?ZV-K8U{QR_C^OxZP1t;j~5IHw@#E1y6(7J(hdF!u59 zE^?g87HHtyg>H-FzUgDUt&mq1qbtO&nLJ9by22oI#L<5B8(`mJd9X4(s?sWFWKvVk zxf?6o8vAn9z$c|HkbMaBIWiH&U+@O!sE-{E-ALhz!v0M8(D9F$L8y(0@P*&?sYA8XS977gU?1Ps!A=zORgJAH}?`Bxjqa@@hsc= zMpyRsy#2qh0BdDYN&^~OBc-pZ0lvBC_Ai-)Y4s=Uv6ndJM}@4tHEjq+^x5xTYdq|bzH&{bO5^sr75Kk1|a zw`u)EAuSE?62pfojL)&S$nBmNfU zjERFMEI#aZOZ)2j=3rec-=;twPmLBCjkycWog;RFq70G;+k!B_Owh58*+8GT7@7vx!QAD@{a;@p$n%FO8K z;pOs^ip?f2k4o`&0m|VxnOkGee!vGn_def5?5feBFRJ_9;Y5+m8cL8^-Rj;`HVy9G zoN#^*b7qxN55a6Hx@s;hTUgT{4_8; zk$(M!Uli{wXlP8jjFqwbw#JYKDacFxsmTkZ^%D;=X`k0S<&ChD*mE_Oox{bsp_XLO zD2LMA%D)Ui#6(beTCO1G-4=VHW^-M%?)V7Ej~FQ)IY*RGjzL_2rpF4z4#7Hu;w*J| z2kK!KUX~2FuJbG}xKbkEaVh#)>9E);w+gW%$AiUg^ux_Fl8MDz;VdGaOD}ea2*%NI zMN{H`h4|@(uA;oUMK4FW^^u%E_5Qw~w6d4Emxr@#JgU8XYM3ew8c0<+6hy#rDNTtH ze-hSS-yP0nJKX8Px*M6dRrDDWohLSyI531KMIro2{tFLTQ+&sve?;>As|#BiyA&~l zw4%KtT<<0-Y+P4Y$mjSTuuZIQI=OH%X_fyXoOW&YDTQ*?fmg<#4=2?=Kb(ljQsC#a z+#&T#Zeeu3b5eh4H|b(6a&YKu>dnq0U>S*P3?%RHK~VQ7&*v~%$U19tCQ=$;r!!W_ zRZ?`3^U5qw-MY1Zy$;Pa?6@mYcA}%u5{)9$XI5=*zco%RMt+sGy|Mq9OOt~J{!jh4pEusII5xZCLi~sJYv#+neT~m~o*He~=Kdh%j&pr^ACu6WS zG67NvG~S2Q)wFr!e75UbwPew{A=*iTeSb+D2X9RDL_yAsOw>e?!|CfwSTlZKtrp~RrvB?fGZ)@+rSuQ9P^z9M zyP-_fO8R@}qc$PCfnwlp+0}O-Tufgd%0Vt08BR+qAb0yCa#uT}a^Es*b-V$}bniqK zm*p)G)nC>Or$4wSUDeN=k{cPxKkK`6y!d$3Y^3nRRsjn-y2b8jrqVXbE?-?&fz89P z`2IO(-7KxD7|Pyc*|x^tfZRRo?dgXC!Rwvn$ju${Q6}}7&q2%2-hV9j2QpETNvkR< zFt7~!n{HmU;`$3hl{`yj;C_5cZIT4(JcN%U3F}18A0h3U%1iD|&P**QKj74vu*i8u zuEy>#v(zNgn+v^vB(w(Dx#68Hk3|R04x)Ugqd0}5<$DfS(rP3|4s%{5BImzsiT%h#A&S#g2fCRoUeh;;Z1}%ZjozPYxyb;rL+-n!?A&|L1hm|A4o@u}706F6Yfm0_R2B~agN|AMwNLvIGX zebDnFiY={3zwy$1^Im>+r*vL1Thc2@zfj4=S;RdFE%diW>5V`X(bNW67s~mfQhbOD zqu7^=)CM!QMtv35Ur>lM{~LA{p@zwGc%IO?frmI-_@&qd+W0Pa)f-n*$rS) zH7Gx>ebe9DJBH7m&XxLNN`+s~lHnvyr-UV;6E_(xa)qK8)MP$@yynYljG9m7kBqk? zS3Snm@7&uZ5PYdp13^^t6kYjsy(;9FU|W&gYFt@w<9Yn_fn+dnJA)%_o`kn%$U`F| z_mAD`6%3 ziHv1DN-|1W#$+eP*kw(FbD6{Bs~tfegAA!54u6gX zaoKnTUtU(`(vy|^;>Mrb9xC(p4PaiA=N{p31;8o2xS#Z-Or3w|f<9^syDJ-Eps)Gr zir~_#YG0p?DSX!9)@( z@7;%3qMEN2PKmB9TUwFSv*A~r@r3AsQjdt>;O$_g5C|uAg}O(7uU^2k9`A(Q=S8a0 z!2Bg#HJHx#?4=9!IojVYcKi_~UzXFfucf{oI==e0)C6y?Ic~rc=G0{Fe&XKtwe9B| z8ifP|=2{bCV}EXpB-BY*p~<~IW2LbVHNcIrYE!kP4Amk6)%H&&jC}G`2c)eleHDRu zpXBoz>@{x8O!`{!tb4WEF&!^}l#&w|DVwY!wTQ#o^s=6H!7c0lcb_iV&q_IT4F@Wn zYwwsRH^4I0bVAIKVJHiz>(r^*?2KRM$_S&4=Q2wd7Z*qRZDT^Q3k&{GaB~7asCT*Q z?s5v)4@Q4LKKFR1-`IG%-(*KN#V2qrM$&#z6oVoUC6xULoS&VB%wPzf-!5O-{IT&< zXHT#T1ZIaWPLlnG*iWhjO!#27aC}35jSMYL;cvcN1*3O76r-cGp`EmX(dq5*L{>&J zRfnsfuK^^#-Ie%|>@wofK3^v7nKxw`Aw?tnp+@?hqUBYY^~-yd&cSTrQf*b zyfm;dTsui>DJXDNlqxB1G>_}bGAq(q(7@rCubOuK&sbQFA^P)wg@sw)-x}Yyv^3Rd z{&RCvc1?p)uKT>~af;FvXJ6mS!%rsXh;{~MAVGaVvc{AXg z>HN4_{J9(-x0%E0>a{>TF0I&p5#gHqvm2se>vWU(6jNn+JZC7jovH19o9~Liw~(de zulZ)b<=WcXF2jU=EerqP2yf*6Kdt~4RP>e*nM^rus6^}N3+Ha;>1y^l9&F%v|HmCJ zCo8!Mm;9WN89B(gRXOv3`{#%qI`w`&KI#P`L<%@F$%KMFcC0aErY65TyDaev$nSEc z%R=0=uj#WtmMAk2)eGRF@G$ahk-HQ7_B1bTXXQq8BJsoc_>Tmslcq5mKSmDa-Gn97 zZ;!yFPve!RncJb+Jic>^V-%YV1w6e+-%dJi!QQzwH~J>Sd`e!K%u8& zt0{YXYG^ysvS|O470uQj&)MNVZ1Sk`a>DKRFfP*1`iy3vFOv(uVW!_1la)Ltu6cWN z8!OpgIo7mnUF{Jq4b?gjuNG5Q^CqvJi~1y}p_?q^l;96v2^24w%-?U6|sy*ID(6J((#mja#B z5STW<=@pbLHu||pr%m>Fe{TgFF$`*Eeb?J)ZFU%dI`~xlLbzdI0g5=XxR|y;aP{5{ zNc5P6u;|x!o$xbTCgRUg%O?dTQk~>IX>I^h=cy%TP%@V8@Ino8G$2O6KQmB#j9 zU9D?JZKMEee32m%xZ^$s?QS-Yt|l&DNh)<5PPL-B;-kD64ZuOpPB&*I0>I`MG-C`N zkGfGj=ae&%B;e0!>}eVoYm{?Kuj57_I(_YK;}L>AsfubthL$feF&zN`;x1XJ3x6u8 zrgmS#hm%qv_Nf`ywZVv`-eVUkox}oo|A>*ooi+>(SZm~ zWC=MjF{hv)lbFrlr&c#+XH}rlfwFzLm6rNUZ+p6%28BS&Up&htoJ!+qYoitbc^V*< z^DS_wKD?{8BoxUFKKh6yMjc@Ug0bIfxnIZ3JP`a@_5mfJs%?M(mgO%__9A=R`uh~} z`b$F%oxo|`RM_Pbe&-8b+mfpEV#rF%- zlhNvk#@b@O=C(6BPQM>oy zvev9|X;Jy7suUA(*OtULQ46AbrGAl8I;s?(o{bJkC5{B(cpJIwR?#G=03n9z#8sh2 zjb6%2=ho+4{%8`6tS(lg8`&f&W1HW_4eiUYACv;O6KEs)F@djqyRsd+5|1=ya#gr? zJXqIVy(+tqTy^+F9EMtY!Fjypby<$#ERhN2IA67O1}P-eo^PuD`ZA;{#jFb`tfE=8 zOO1ApW{}O!c+13I_AXrS5pkoW$qi7m%|!e34wx3j?bO#vnkh8NWq}D=JuK}{8%2U)zR%ce0J?STx+Hdt_z4RQNkC*q>ToUG-|Cv(dKMDKmjrUn&UlRFz zn}7N*5AhwjK~LJtYgkV9UQf$iuR0L;=SH_94VF}6n1?$tBu7- zyUaPMSi>P;+h`O5ON~=Q$hXgK(}&@E>8WrB77{88DYH zd)t4&T%$WC@~o>Md&P(Ez}jJ>&vvXVbCS8nXHWyaT0A*3;^q}?vdi3xpb%LsBo!2( z9FzIfteLJ2)fH$_qFyh?47AQ%rNzz29t4hM+m}=Fqqa9=K6OI29@$d(jowXE4bW`g zNotE*HBG^C=<`fA$-Np&@5;rvEkQ`slaK8wyOk%gR2%7GqpCj!%-MW$%tx)*fa`VB zV$DqfOU94)Etx*vzk7c(k3TY?{7&}@@EHV`-dqc6>~FZry6M#NMkh4MqD-`~UlJ<% zMVBIY`k$g&rg)e~R;XyMIork~M~kuBfOCeVq?v#{cN%=yg?WHN*ABgl(M)Q8S@c1U zaTTHqroTTOefC+=M3Z%YgU`+B&d&ukI~RW0C-20zzSnAfyL<7?eE^(Za(|~B`c7O2 znEY)RwhnPz1)H>N%2n8BQ#TDI9om%lO-ikDjd?&O8%$&=x2rP zTWG8$guL&9&t}<)5!d3rTg^@5C%Gn-=MOaN*hz#j7P9w4+UHR)zYry^$3#X_Bvw>c z@J-Ky-d-PXXS7j)JO%mfMd~x{REmj?4&D+$fy4-Gb@F-Wk13KQeT2EO#gbfWy+c&1 zcx_vP^w(;4OQ5nsrW9Avjo~J)u?2k;t;~bZxz4;dR07Q<>T&j`$~eNP~6u` zDjddyZtH`QLe60Sy6OU1Vw2>&sTuQ?SUaz^hX>hXJz0AEQi33-q-ozBgd8ElDsp0S zs@U0x)2hTy0^b44r04<2u0EJ1 zZV0N~-kwkp;k}e2Ct?d+Xm2I`(SX-q^Q?iRE|5}6Z2k0e;lM_I9h34>#L=^EOU1r8 z+(pG#tjuiFSk@or(v|l%T|jCKR>=&9IbB}4>$M*(7w@Q@CAfg`;zjAXEyO=VL}N1l z86uJe%%WFC=z*0>9XTC4t>ThZ$aC~q({0+xol9%|V2sN(dZvO%{&>drFS7upV~w8| z`%+;P#7vh7K8NrT_}q{w5eRiJCrRStF*IC*rIgF{nF1)ELWErJc@HToLh(LMUmv69 z+g0qQzL|!1Ue>7l1WFiku3J?2Ew?CSZE4sJrXcpe(%RZqwL2d^40#HrO+6XZypqcm z5mPMAH#>UrIiXYb2qE)ewM(1sR+yzi+0a*`ozpbWgDGL_%j0M7z2)=YZU2E!*1AB0 zh;&-NRKo+2ysQ7@d8pAg#%4HwN+|+Xpe)X!PuLhvaI^J+Fx0LUTs|5d4cSDVxS9Q z8~T&@O0Q7!PrO*As$3xOcmvNXiS%t{+K%~F=bqH8jy*r$mY3>ysO96ro_o}u@wwnGe zA#uZTsI#yAdJ=Y{zeCgq^;My8S(>NRae!!DlX(oeUfcqvN9^S1_yMhf3$`om+E4^! znF|H>UsL=Jc${$ZaNpBl!KS+F74is`I{P{6JciN7`wz&@M7+RhGXX3rEL;x=H@vd* z&$o=U^FEK&+jNJi+9S}oT?ZE#5b@g_82PNJPR56B-?tiy7 z@oqP4)O^>*j(29h*)X0gAC{V|JcR8ih(kxr%`U=)bn{3)bny`sTZcqr%~46}wv%5@ z3`EjSKjovC#9)&F%IR)~#?Hk`4Sg)~uv?U^jSzQdmRZ>TNuIIE*bI6qG9y0b?Aj_s zpW=!k3JNQ|RLn(8HwwwH(lx9i+N>t~X-9HpgPP(<^x6@;@)=F+g5XoWh8_;zJ6Gxc z(l1zkmYcL5bD`#?eM*)uzjJqF%BD%8^_a=#!zIM!t+^${kPKm^YGlka(fz`WC>$?`h`P{9qt<9cmJrfl9Su=Kd-p2* zIz)|4K)~2y&e*^(V=UN_3?C(7DECC+2OPOUXbtXX)yP9Bi(%2c>9K@5-hmsLSsMpF zlEsSrjP2a1vhw1Quw`Zvx^eB2fiYVh=Qz_2b!%$;#M^0UBmDTQ3pA3^uUI@Lb&Ww% z`!EL^Gv{5Lh-OW#PNaDqp&3urKZFfu34aXxMR;-^&(>diO-g-tdKs~FSVFyMQ2Shm z(KwZ4WPrdi$_Vt&ojOJIN9ja!nT#1-yYZL)oY+-3L4@_h0OPD{7IgxYmWj@fLy#$K<45CA zxcT@fk3z))6N`PE6gjeo9Q~}j$7{3O?V7r5%pvNGfIlWW8xDbt(%LT{Kf!VB+Qf*1 zf~vd0rLL6do7TSS>dclp(wHL&&v=iA9D+FncEPIr4_@q+_R`KqYQ@f|_lsrX290)Q zyXt$uWg)g3XLGV0<9qZPY=lf3e0C0=95B-Bo13fYm>NVjS$^6!(m$9bWMvz_>2Ru& z8wLJn#Z3o|1BaA}G=i7`<{@6ok(t>-h@rmQvV?XBWeVHW#0R5?XcOZo-eAI-Y?B;}HXLI+W^+cSy`!u1mkNCA7}Y(@#=t!$VC2IB zro#x&IpT3hB2%8jTK(LLN7Ph8w6AB0D+Xil!} z8Yg&p5MML`6|T3zL0Ab>x5B-;E!@bR)5yZ&p;;rK5xJk7X!O%=jv!xLEVN}WNXn#Q zpKqM86Hs^7xyxxMU?;!~dKWvpITI;H7Q_h^4q+NiiY6&Jc282E@70Il#m83yMbueX zrR|s-ysYACo1S69%U&G!m&;xyM=grDyi`n#!;7JsQh7E_!bL3LJ>Isas=}j`^&&&F zE*d$F+Af~Zl2Fnp96DLbiLE`c;!)XcVn3?p5_(VdwiyPa(NgIamsu?e3%p%hT9+;u zP0@edEUW2EB84YNCy=9!BK7FIga{tbsFfy!Ykwsxd@)r6ixyFdIC}e-wuyH+ZxcDn zyF4Bj70)7=Thn`N z9{kFrf8LkzFPT)C{#C>5-^Vi^mVe!7{P%hL`@H>q-~9dD{5=l*{~ian03)H(`Iz>c hhGBjuVF{?g+Uc)@)r=WaOI`O;!Nu#r{|2WQj!ggn diff --git a/docs/pr-1978/tissue-loading-before.png b/docs/pr-1978/tissue-loading-before.png index b3c320ccfc3fc17dc3007f5e40b12f6e31e9eb23..4f63b36c3fce8b77c8c31ea15b060f2d92f2c5ed 100644 GIT binary patch literal 13127 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 TK869mgaJu@k`pTv(fjrvS#lN) literal 21307 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