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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 33 additions & 2 deletions lib/core/services/export/uddf/uddf_full_import_service.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:xml/xml.dart';

import 'package:submersion/core/constants/enums.dart' as enums;
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_import_parsers.dart';
import 'package:submersion/core/services/export/uddf/uddf_normalizer.dart';
Expand All @@ -13,6 +14,8 @@ import 'package:submersion/features/dive_log/domain/entities/dive.dart';
/// Delegates base parsing to [UddfImportService] and entity parsing
/// to [UddfImportParsers].
class UddfFullImportService {
static final _logger = LoggerService.forClass(UddfFullImportService);

/// Import ALL application data from UDDF file.
/// Returns [UddfImportResult] with all parsed data.
Future<UddfImportResult> importAllDataFromUddf(String uddfContent) async {
Expand Down Expand Up @@ -1244,9 +1247,16 @@ class UddfFullImportService {
final tankInfo = <String, dynamic>{};

// Capture tank ID for reference mapping (used by waypoint tankpressure refs)
final tankId = tankDataElement.getAttribute('id');
if (tankId != null) {
final rawTankId = tankDataElement.getAttribute('id');
final tankId = rawTankId?.trim();
if (tankId != null && tankId.isNotEmpty) {
tankInfo['uddfTankId'] = tankId;
} else {
_logger.debug(
'UDDF import: <tankdata> is missing or has empty/whitespace '
'required "id" attribute; '
'falling back to ordered tank ref resolution.',
);
}

// Get tank volume (in liters)
Expand Down Expand Up @@ -1402,6 +1412,12 @@ class UddfFullImportService {
tankRefToIndex[uddfTankId] = i;
}
}
final fallbackTankIndices = <int>[
for (var i = 0; i < tanks.length; i++)
if (tanks[i]['uddfTankId'] == null) i,
];
final fallbackRefToIndex = <String, int>{};
var nextFallbackTankIndex = 0;

if (tanks.isNotEmpty) {
diveData['tanks'] = tanks;
Expand Down Expand Up @@ -1462,6 +1478,21 @@ class UddfFullImportService {
int tankIdx;
if (tankRef != null && tankRefToIndex.containsKey(tankRef)) {
tankIdx = tankRefToIndex[tankRef]!;
} else if (tankRef != null) {
final fallbackTankIndex = fallbackRefToIndex[tankRef];
if (fallbackTankIndex != null) {
tankIdx = fallbackTankIndex;
} else if (nextFallbackTankIndex < fallbackTankIndices.length) {
tankIdx = fallbackTankIndices[nextFallbackTankIndex++];
fallbackRefToIndex[tankRef] = tankIdx;
} else {
_logger.debug(
'UDDF import: ${tanks.length} tank records but '
'${fallbackRefToIndex.length + 1} unique unmatched tank refs; '
'dropping ref "$tankRef" from import.',
);
continue;
}
} else {
// Default to primary tank (index 0) when no ref attribute
tankIdx = 0;
Expand Down
35 changes: 33 additions & 2 deletions lib/core/services/export/uddf/uddf_import_service.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:xml/xml.dart';

import 'package:submersion/core/constants/enums.dart' as enums;
import 'package:submersion/core/services/logger_service.dart';
import 'package:submersion/core/services/export/uddf/uddf_import_parsers.dart';
import 'package:submersion/features/dive_log/domain/entities/dive.dart';

Expand All @@ -9,6 +10,8 @@ import 'package:submersion/features/dive_log/domain/entities/dive.dart';
/// Parses standard UDDF elements (diver, divesite, gasdefinitions,
/// decomodel, profiledata) and returns dive and site maps.
class UddfImportService {
static final _logger = LoggerService.forClass(UddfImportService);

Future<Map<String, List<Map<String, dynamic>>>> importDivesFromUddf(
String uddfContent,
) async {
Expand Down Expand Up @@ -367,9 +370,16 @@ class UddfImportService {
final tankInfo = <String, dynamic>{};

// Capture tank ID for reference mapping (used by waypoint tankpressure refs)
final tankId = tankDataElement.getAttribute('id');
if (tankId != null) {
final rawTankId = tankDataElement.getAttribute('id');
final tankId = rawTankId?.trim();
if (tankId != null && tankId.isNotEmpty) {
tankInfo['uddfTankId'] = tankId;
} else {
_logger.debug(
'UDDF import: <tankdata> is missing or has empty/whitespace '
'required "id" attribute; '
'falling back to ordered tank ref resolution.',
);
}

// Get tank volume (in liters)
Expand Down Expand Up @@ -510,6 +520,12 @@ class UddfImportService {
tankRefToIndex[uddfTankId] = i;
}
}
final fallbackTankIndices = <int>[
for (var i = 0; i < tanks.length; i++)
if (tanks[i]['uddfTankId'] == null) i,
];
final fallbackRefToIndex = <String, int>{};
var nextFallbackTankIndex = 0;

if (tanks.isNotEmpty) {
diveData['tanks'] = tanks;
Expand Down Expand Up @@ -567,6 +583,21 @@ class UddfImportService {
int tankIdx;
if (tankRef != null && tankRefToIndex.containsKey(tankRef)) {
tankIdx = tankRefToIndex[tankRef]!;
} else if (tankRef != null) {
final fallbackTankIndex = fallbackRefToIndex[tankRef];
if (fallbackTankIndex != null) {
tankIdx = fallbackTankIndex;
} else if (nextFallbackTankIndex < fallbackTankIndices.length) {
tankIdx = fallbackTankIndices[nextFallbackTankIndex++];
fallbackRefToIndex[tankRef] = tankIdx;
} else {
_logger.debug(
'UDDF import: ${tanks.length} tank records but '
'${fallbackRefToIndex.length + 1} unique unmatched tank refs; '
'dropping ref "$tankRef" from import.',
);
continue;
}
} else {
// Default to primary tank (index 0) when no ref attribute
tankIdx = 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,7 @@ class _DiveProfileChartState extends ConsumerState<DiveProfileChart> {
_showCeiling = widget.showCeiling;
_showAscentRateColors = widget.showAscentRateColors;
_showEvents = widget.showEvents;
_scheduleTankPressureVisibilityInitialization();
}

@override
Expand All @@ -377,6 +378,18 @@ class _DiveProfileChartState extends ConsumerState<DiveProfileChart> {
_lastTooltipSpotIndex = null;
_lastTooltipItems = [];
}
if (oldWidget.tankPressures != widget.tankPressures) {
_scheduleTankPressureVisibilityInitialization();
}
}

void _scheduleTankPressureVisibilityInitialization() {
if (!_hasMultiTankPressure) return;
final tankIds = widget.tankPressures!.keys.toList();
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted || tankIds.isEmpty) return;
ref.read(profileLegendProvider.notifier).initializeTankPressures(tankIds);
});
}

void _resetZoom() {
Expand Down Expand Up @@ -1988,7 +2001,7 @@ class _DiveProfileChartState extends ConsumerState<DiveProfileChart> {
final tankId = sortedTankIds[i];

// Skip if tank is hidden
if (!(_showTankPressure[tankId] ?? true)) continue;
if (_showTankPressure[tankId] == false) continue;

final pressurePoints = tankPressures[tankId]!;
if (pressurePoints.isEmpty) continue;
Expand Down
163 changes: 163 additions & 0 deletions test/core/services/export/uddf/uddf_full_import_service_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:submersion/core/services/export/uddf/uddf_full_import_service.dart';

void main() {
group('UddfFullImportService', () {
test(
'maps tankpressure refs by tank order when tankdata entries omit ids',
() async {
const uddfContent = '''
<uddf version="3.2.3">
<profiledata>
<repetitiongroup>
<dive id="dive-1">
<informationbeforedive>
<datetime>2025-09-01T14:18:24Z</datetime>
<divenumber>235</divenumber>
</informationbeforedive>
<tankdata>
<tankpressurebegin>20049962</tankpressurebegin>
<tankpressureend>12879411</tankpressureend>
</tankdata>
<tankdata>
<tankpressurebegin>21952916</tankpressurebegin>
<tankpressureend>14244574</tankpressureend>
</tankdata>
<samples>
<waypoint>
<depth>1</depth>
<divetime>0</divetime>
<tankpressure ref="o2">20049962</tankpressure>
<tankpressure ref="he">21952916</tankpressure>
</waypoint>
</samples>
</dive>
</repetitiongroup>
</profiledata>
</uddf>
''';

final service = UddfFullImportService();

final result = await service.importAllDataFromUddf(uddfContent);
expect(result.dives, hasLength(1));

final dive = result.dives.first;
final tanks = dive['tanks'] as List<Map<String, dynamic>>;
final profile = dive['profile'] as List<Map<String, dynamic>>;
final firstPointPressures =
profile.first['allTankPressures'] as List<Map<String, dynamic>>;

expect(tanks, hasLength(2));
expect(tanks[0]['uddfTankId'], isNull);
expect(tanks[1]['uddfTankId'], isNull);

expect(firstPointPressures, hasLength(2));
expect(firstPointPressures[0]['tankIndex'], 0);
expect(firstPointPressures[1]['tankIndex'], 1);
expect(firstPointPressures[0]['pressure'], closeTo(200.5, 0.1));
expect(firstPointPressures[1]['pressure'], closeTo(219.5, 0.1));
},
);

test(
'treats empty and whitespace tankdata ids as missing for fallback mapping',
() async {
const uddfContent = '''
<uddf version="3.2.3">
<profiledata>
<repetitiongroup>
<dive id="dive-1">
<informationbeforedive>
<datetime>2025-09-01T14:18:24Z</datetime>
<divenumber>235</divenumber>
</informationbeforedive>
<tankdata id="">
<tankpressurebegin>20049962</tankpressurebegin>
<tankpressureend>12879411</tankpressureend>
</tankdata>
<tankdata id=" ">
<tankpressurebegin>21952916</tankpressurebegin>
<tankpressureend>14244574</tankpressureend>
</tankdata>
<samples>
<waypoint>
<depth>1</depth>
<divetime>0</divetime>
<tankpressure ref="T1">20049962</tankpressure>
<tankpressure ref="T2">21952916</tankpressure>
</waypoint>
</samples>
</dive>
</repetitiongroup>
</profiledata>
</uddf>
''';

final service = UddfFullImportService();

final result = await service.importAllDataFromUddf(uddfContent);
expect(result.dives, hasLength(1));

final dive = result.dives.first;
final tanks = dive['tanks'] as List<Map<String, dynamic>>;
final profile = dive['profile'] as List<Map<String, dynamic>>;
final firstPointPressures =
profile.first['allTankPressures'] as List<Map<String, dynamic>>;

expect(tanks, hasLength(2));
expect(tanks[0]['uddfTankId'], isNull);
expect(tanks[1]['uddfTankId'], isNull);
expect(firstPointPressures, hasLength(2));
expect(firstPointPressures[0]['tankIndex'], 0);
expect(firstPointPressures[1]['tankIndex'], 1);
},
);

test('drops extra unmatched refs beyond available tank records', () async {
const uddfContent = '''
<uddf version="3.2.3">
<profiledata>
<repetitiongroup>
<dive id="dive-1">
<informationbeforedive>
<datetime>2025-09-01T14:18:24Z</datetime>
<divenumber>235</divenumber>
</informationbeforedive>
<tankdata>
<tankpressurebegin>20049962</tankpressurebegin>
<tankpressureend>12879411</tankpressureend>
</tankdata>
<tankdata>
<tankpressurebegin>21952916</tankpressurebegin>
<tankpressureend>14244574</tankpressureend>
</tankdata>
<samples>
<waypoint>
<depth>1</depth>
<divetime>0</divetime>
<tankpressure ref="o2">20049962</tankpressure>
<tankpressure ref="he">21952916</tankpressure>
<tankpressure ref="argon">15000000</tankpressure>
</waypoint>
</samples>
</dive>
</repetitiongroup>
</profiledata>
</uddf>
''';

final service = UddfFullImportService();

final result = await service.importAllDataFromUddf(uddfContent);
final dive = result.dives.first;
final profile = dive['profile'] as List<Map<String, dynamic>>;
final firstPointPressures =
profile.first['allTankPressures'] as List<Map<String, dynamic>>;

expect(firstPointPressures, hasLength(2));
expect(firstPointPressures[0]['tankIndex'], 0);
expect(firstPointPressures[1]['tankIndex'], 1);
});
});
}
Loading
Loading