Skip to content

Commit 0105055

Browse files
committed
issue-70 UDDF dual-tank only shows one tank on dive profile
The provided UDDF file is non-conformant with the UDDF specification, which requires the <tankdata> element to have an id attribute. This change adds ordered fallback mapping during import when tankdata ids are missing, so tankpressure refs can still be associated with the correct tanks.
1 parent ec82a5c commit 0105055

7 files changed

Lines changed: 581 additions & 1 deletion

File tree

lib/core/services/export/uddf/uddf_full_import_service.dart

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'package:xml/xml.dart';
22

33
import 'package:submersion/core/constants/enums.dart' as enums;
4+
import 'package:submersion/core/services/logger_service.dart';
45
import 'package:submersion/core/services/export/models/uddf_import_result.dart';
56
import 'package:submersion/core/services/export/uddf/uddf_import_parsers.dart';
67
import 'package:submersion/core/services/export/uddf/uddf_normalizer.dart';
@@ -13,6 +14,8 @@ import 'package:submersion/features/dive_log/domain/entities/dive.dart';
1314
/// Delegates base parsing to [UddfImportService] and entity parsing
1415
/// to [UddfImportParsers].
1516
class UddfFullImportService {
17+
static final _logger = LoggerService.forClass(UddfFullImportService);
18+
1619
/// Import ALL application data from UDDF file.
1720
/// Returns [UddfImportResult] with all parsed data.
1821
Future<UddfImportResult> importAllDataFromUddf(String uddfContent) async {
@@ -1247,6 +1250,11 @@ class UddfFullImportService {
12471250
final tankId = tankDataElement.getAttribute('id');
12481251
if (tankId != null) {
12491252
tankInfo['uddfTankId'] = tankId;
1253+
} else {
1254+
_logger.debug(
1255+
'UDDF import: <tankdata> is missing required "id" attribute; '
1256+
'falling back to ordered tank ref resolution.',
1257+
);
12501258
}
12511259

12521260
// Get tank volume (in liters)
@@ -1402,6 +1410,12 @@ class UddfFullImportService {
14021410
tankRefToIndex[uddfTankId] = i;
14031411
}
14041412
}
1413+
final fallbackTankIndices = <int>[
1414+
for (var i = 0; i < tanks.length; i++)
1415+
if (tanks[i]['uddfTankId'] == null) i,
1416+
];
1417+
final fallbackRefToIndex = <String, int>{};
1418+
var nextFallbackTankIndex = 0;
14051419

14061420
if (tanks.isNotEmpty) {
14071421
diveData['tanks'] = tanks;
@@ -1462,6 +1476,21 @@ class UddfFullImportService {
14621476
int tankIdx;
14631477
if (tankRef != null && tankRefToIndex.containsKey(tankRef)) {
14641478
tankIdx = tankRefToIndex[tankRef]!;
1479+
} else if (tankRef != null) {
1480+
final fallbackTankIndex = fallbackRefToIndex[tankRef];
1481+
if (fallbackTankIndex != null) {
1482+
tankIdx = fallbackTankIndex;
1483+
} else if (nextFallbackTankIndex < fallbackTankIndices.length) {
1484+
tankIdx = fallbackTankIndices[nextFallbackTankIndex++];
1485+
fallbackRefToIndex[tankRef] = tankIdx;
1486+
} else {
1487+
_logger.debug(
1488+
'UDDF import: ${tanks.length} tank records but '
1489+
'${fallbackRefToIndex.length + 1} unique unmatched tank refs; '
1490+
'dropping ref "$tankRef" from import.',
1491+
);
1492+
continue;
1493+
}
14651494
} else {
14661495
// Default to primary tank (index 0) when no ref attribute
14671496
tankIdx = 0;

lib/core/services/export/uddf/uddf_import_service.dart

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'package:xml/xml.dart';
22

33
import 'package:submersion/core/constants/enums.dart' as enums;
4+
import 'package:submersion/core/services/logger_service.dart';
45
import 'package:submersion/core/services/export/uddf/uddf_import_parsers.dart';
56
import 'package:submersion/features/dive_log/domain/entities/dive.dart';
67

@@ -9,6 +10,8 @@ import 'package:submersion/features/dive_log/domain/entities/dive.dart';
910
/// Parses standard UDDF elements (diver, divesite, gasdefinitions,
1011
/// decomodel, profiledata) and returns dive and site maps.
1112
class UddfImportService {
13+
static final _logger = LoggerService.forClass(UddfImportService);
14+
1215
Future<Map<String, List<Map<String, dynamic>>>> importDivesFromUddf(
1316
String uddfContent,
1417
) async {
@@ -370,6 +373,11 @@ class UddfImportService {
370373
final tankId = tankDataElement.getAttribute('id');
371374
if (tankId != null) {
372375
tankInfo['uddfTankId'] = tankId;
376+
} else {
377+
_logger.debug(
378+
'UDDF import: <tankdata> is missing required "id" attribute; '
379+
'falling back to ordered tank ref resolution.',
380+
);
373381
}
374382

375383
// Get tank volume (in liters)
@@ -510,6 +518,12 @@ class UddfImportService {
510518
tankRefToIndex[uddfTankId] = i;
511519
}
512520
}
521+
final fallbackTankIndices = <int>[
522+
for (var i = 0; i < tanks.length; i++)
523+
if (tanks[i]['uddfTankId'] == null) i,
524+
];
525+
final fallbackRefToIndex = <String, int>{};
526+
var nextFallbackTankIndex = 0;
513527

514528
if (tanks.isNotEmpty) {
515529
diveData['tanks'] = tanks;
@@ -567,6 +581,21 @@ class UddfImportService {
567581
int tankIdx;
568582
if (tankRef != null && tankRefToIndex.containsKey(tankRef)) {
569583
tankIdx = tankRefToIndex[tankRef]!;
584+
} else if (tankRef != null) {
585+
final fallbackTankIndex = fallbackRefToIndex[tankRef];
586+
if (fallbackTankIndex != null) {
587+
tankIdx = fallbackTankIndex;
588+
} else if (nextFallbackTankIndex < fallbackTankIndices.length) {
589+
tankIdx = fallbackTankIndices[nextFallbackTankIndex++];
590+
fallbackRefToIndex[tankRef] = tankIdx;
591+
} else {
592+
_logger.debug(
593+
'UDDF import: ${tanks.length} tank records but '
594+
'${fallbackRefToIndex.length + 1} unique unmatched tank refs; '
595+
'dropping ref "$tankRef" from import.',
596+
);
597+
continue;
598+
}
570599
} else {
571600
// Default to primary tank (index 0) when no ref attribute
572601
tankIdx = 0;

lib/features/dive_log/presentation/widgets/dive_profile_chart.dart

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,15 @@ class _DiveProfileChartState extends ConsumerState<DiveProfileChart> {
457457
_showTankPressure[entry.key] = entry.value;
458458
}
459459

460+
// Initialize tank pressure visibility for any tanks not yet in the legend state
461+
if (_hasMultiTankPressure && widget.tankPressures != null) {
462+
for (final tankId in widget.tankPressures!.keys) {
463+
if (!_showTankPressure.containsKey(tankId)) {
464+
_showTankPressure[tankId] = true;
465+
}
466+
}
467+
}
468+
460469
// Check data availability for advanced curves
461470
final hasNdlData = widget.ndlCurve != null && widget.ndlCurve!.isNotEmpty;
462471
final hasPpO2Data =
@@ -1988,7 +1997,7 @@ class _DiveProfileChartState extends ConsumerState<DiveProfileChart> {
19881997
final tankId = sortedTankIds[i];
19891998

19901999
// Skip if tank is hidden
1991-
if (!(_showTankPressure[tankId] ?? true)) continue;
2000+
if (_showTankPressure[tankId] == false) continue;
19922001

19932002
final pressurePoints = tankPressures[tankId]!;
19942003
if (pressurePoints.isEmpty) continue;
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import 'package:flutter_test/flutter_test.dart';
2+
import 'package:submersion/core/services/export/uddf/uddf_full_import_service.dart';
3+
4+
void main() {
5+
group('UddfFullImportService', () {
6+
test(
7+
'maps tankpressure refs by tank order when tankdata entries omit ids',
8+
() async {
9+
const uddfContent = '''
10+
<uddf version="3.2.3">
11+
<profiledata>
12+
<repetitiongroup>
13+
<dive id="dive-1">
14+
<informationbeforedive>
15+
<datetime>2025-09-01T14:18:24Z</datetime>
16+
<divenumber>235</divenumber>
17+
</informationbeforedive>
18+
<tankdata>
19+
<tankpressurebegin>20049962</tankpressurebegin>
20+
<tankpressureend>12879411</tankpressureend>
21+
</tankdata>
22+
<tankdata>
23+
<tankpressurebegin>21952916</tankpressurebegin>
24+
<tankpressureend>14244574</tankpressureend>
25+
</tankdata>
26+
<samples>
27+
<waypoint>
28+
<depth>1</depth>
29+
<divetime>0</divetime>
30+
<tankpressure ref="o2">20049962</tankpressure>
31+
<tankpressure ref="he">21952916</tankpressure>
32+
</waypoint>
33+
</samples>
34+
</dive>
35+
</repetitiongroup>
36+
</profiledata>
37+
</uddf>
38+
''';
39+
40+
final service = UddfFullImportService();
41+
42+
final result = await service.importAllDataFromUddf(uddfContent);
43+
expect(result.dives, hasLength(1));
44+
45+
final dive = result.dives.first;
46+
final tanks = dive['tanks'] as List<Map<String, dynamic>>;
47+
final profile = dive['profile'] as List<Map<String, dynamic>>;
48+
final firstPointPressures =
49+
profile.first['allTankPressures'] as List<Map<String, dynamic>>;
50+
51+
expect(tanks, hasLength(2));
52+
expect(tanks[0]['uddfTankId'], isNull);
53+
expect(tanks[1]['uddfTankId'], isNull);
54+
55+
expect(firstPointPressures, hasLength(2));
56+
expect(firstPointPressures[0]['tankIndex'], 0);
57+
expect(firstPointPressures[1]['tankIndex'], 1);
58+
expect(firstPointPressures[0]['pressure'], closeTo(200.5, 0.1));
59+
expect(firstPointPressures[1]['pressure'], closeTo(219.5, 0.1));
60+
},
61+
);
62+
63+
test('drops extra unmatched refs beyond available tank records', () async {
64+
const uddfContent = '''
65+
<uddf version="3.2.3">
66+
<profiledata>
67+
<repetitiongroup>
68+
<dive id="dive-1">
69+
<informationbeforedive>
70+
<datetime>2025-09-01T14:18:24Z</datetime>
71+
<divenumber>235</divenumber>
72+
</informationbeforedive>
73+
<tankdata>
74+
<tankpressurebegin>20049962</tankpressurebegin>
75+
<tankpressureend>12879411</tankpressureend>
76+
</tankdata>
77+
<tankdata>
78+
<tankpressurebegin>21952916</tankpressurebegin>
79+
<tankpressureend>14244574</tankpressureend>
80+
</tankdata>
81+
<samples>
82+
<waypoint>
83+
<depth>1</depth>
84+
<divetime>0</divetime>
85+
<tankpressure ref="o2">20049962</tankpressure>
86+
<tankpressure ref="he">21952916</tankpressure>
87+
<tankpressure ref="argon">15000000</tankpressure>
88+
</waypoint>
89+
</samples>
90+
</dive>
91+
</repetitiongroup>
92+
</profiledata>
93+
</uddf>
94+
''';
95+
96+
final service = UddfFullImportService();
97+
98+
final result = await service.importAllDataFromUddf(uddfContent);
99+
final dive = result.dives.first;
100+
final profile = dive['profile'] as List<Map<String, dynamic>>;
101+
final firstPointPressures =
102+
profile.first['allTankPressures'] as List<Map<String, dynamic>>;
103+
104+
expect(firstPointPressures, hasLength(2));
105+
expect(firstPointPressures[0]['tankIndex'], 0);
106+
expect(firstPointPressures[1]['tankIndex'], 1);
107+
});
108+
});
109+
}

0 commit comments

Comments
 (0)