Skip to content

Commit f8c4616

Browse files
committed
merge: origin/main into feature/macdive-native-xml (gas-switch pipe + test fix)
Picks up two main commits that landed after the previous merge: - 73dfa00 feat(uddf): persist MacDive waypoint gas switches via existing gasSwitches pipe - ea020e6 fix(uddf): trim mixRef + order-independent tank lookup in test Auto-merge was clean — no conflicts. Touches `uddf_full_import_service`, `uddf_entity_importer`, the MacDive UDDF import test, and adds a new waypoint gas-switch test. Ran the affected suites (1536 tests passing, 5 skipped gated) plus `flutter analyze` and `dart format`.
2 parents 965b39c + ea020e6 commit f8c4616

5 files changed

Lines changed: 321 additions & 37 deletions

File tree

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

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1362,9 +1362,16 @@ class UddfFullImportService {
13621362
// Get linked gas mix
13631363
final mixLink = tankDataElement.findElements('link').firstOrNull;
13641364
if (mixLink != null) {
1365-
final mixRef = mixLink.getAttribute('ref');
1366-
if (mixRef != null && gasMixes.containsKey(mixRef)) {
1367-
tankInfo['gasMix'] = gasMixes[mixRef];
1365+
final rawMixRef = mixLink.getAttribute('ref');
1366+
final mixRef = rawMixRef?.trim();
1367+
if (mixRef != null && mixRef.isNotEmpty) {
1368+
// Record the UDDF gas-mix UUID on the tank so the importer can
1369+
// resolve waypoint-level <switchmix ref> markers (which reference
1370+
// gas mixes, not tanks) back to a tank for the gas_switches row.
1371+
tankInfo['uddfGasMixRef'] = mixRef;
1372+
if (gasMixes.containsKey(mixRef)) {
1373+
tankInfo['gasMix'] = gasMixes[mixRef];
1374+
}
13681375
}
13691376
}
13701377

@@ -1518,6 +1525,11 @@ class UddfFullImportService {
15181525
final samplesElement = diveElement.findElements('samples').firstOrNull;
15191526
if (samplesElement != null) {
15201527
final profile = <Map<String, dynamic>>[];
1528+
// Gas switches emitted from waypoint-level <switchmix ref="..."/>.
1529+
// MacDive marks deco gas changes on individual samples this way; we feed
1530+
// them into the same `diveData['gasSwitches']` pipe as the top-level
1531+
// <gasswitches> section so the importer has one consumer.
1532+
final waypointGasSwitches = <Map<String, dynamic>>[];
15211533
GasMix? currentMix;
15221534
GasMix? pendingSwitchMix;
15231535
double? lastWaypointCns;
@@ -1553,10 +1565,28 @@ class UddfFullImportService {
15531565

15541566
final switchMix = waypoint.findElements('switchmix').firstOrNull;
15551567
if (switchMix != null) {
1556-
final mixRef = switchMix.getAttribute('ref');
1557-
if (mixRef != null) {
1558-
// Record the gas mix reference on the sample for downstream consumers
1559-
point['gasMixRef'] = mixRef;
1568+
final rawMixRef = switchMix.getAttribute('ref');
1569+
final mixRef = rawMixRef?.trim();
1570+
// Skip emission entirely when the ref is empty or whitespace-only:
1571+
// the importer would have no way to resolve such a dangling ref
1572+
// back to a persisted tank row.
1573+
if (mixRef != null && mixRef.isNotEmpty) {
1574+
// Emit a gas switch entry for the importer to persist. Shape
1575+
// matches the top-level <gasswitches> parser (timestamp/depth/
1576+
// tankRef), plus `gasMixRef` so the importer can resolve the
1577+
// MacDive-style gas-UUID reference to a tank.
1578+
final timestamp = point['timestamp'] as int?;
1579+
if (timestamp != null) {
1580+
final entry = <String, dynamic>{
1581+
'timestamp': timestamp,
1582+
'gasMixRef': mixRef,
1583+
};
1584+
final depth = point['depth'];
1585+
if (depth != null) {
1586+
entry['depth'] = depth;
1587+
}
1588+
waypointGasSwitches.add(entry);
1589+
}
15601590

15611591
if (gasMixes.containsKey(mixRef)) {
15621592
currentMix = gasMixes[mixRef];
@@ -1723,6 +1753,24 @@ class UddfFullImportService {
17231753
if (currentMix != null && !diveData.containsKey('tanks')) {
17241754
diveData['gasMix'] = currentMix;
17251755
}
1756+
1757+
// Merge waypoint-level gas switches with any entries emitted earlier
1758+
// from the top-level <gasswitches> section, deduping on
1759+
// timestamp+gasMixRef+tankRef so both paths feed one consumer.
1760+
if (waypointGasSwitches.isNotEmpty) {
1761+
final existing =
1762+
(diveData['gasSwitches'] as List<Map<String, dynamic>>?) ??
1763+
const <Map<String, dynamic>>[];
1764+
final seen = <String>{};
1765+
final merged = <Map<String, dynamic>>[];
1766+
for (final gs in [...existing, ...waypointGasSwitches]) {
1767+
final key = '${gs['timestamp']}|${gs['gasMixRef']}|${gs['tankRef']}';
1768+
if (seen.add(key)) {
1769+
merged.add(gs);
1770+
}
1771+
}
1772+
diveData['gasSwitches'] = merged;
1773+
}
17261774
}
17271775

17281776
// Parse information after dive

lib/features/dive_import/data/services/uddf_entity_importer.dart

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1255,7 +1255,12 @@ class UddfEntityImporter {
12551255
final gasSwitchesData =
12561256
diveData['gasSwitches'] as List<Map<String, dynamic>>?;
12571257
if (gasSwitchesData != null && gasSwitchesData.isNotEmpty) {
1258+
// Build lookups from both UDDF tank ID and UDDF gas mix UUID to the
1259+
// persisted tank row id. MacDive-style switches reference a gas mix
1260+
// UUID (via <switchmix ref>), while top-level <gasswitches>
1261+
// entries reference a tank UUID (via <tankref>); we accept either.
12581262
final tankIdByRef = <String, String>{};
1263+
final tankIdByGasMixRef = <String, String>{};
12591264
final tanksData = diveData['tanks'] as List<Map<String, dynamic>>?;
12601265
if (tanksData != null) {
12611266
for (var i = 0; i < tanks.length && i < tanksData.length; i++) {
@@ -1265,6 +1270,15 @@ class UddfEntityImporter {
12651270
if (ref != null && ref.isNotEmpty) {
12661271
tankIdByRef[ref] = tank.id;
12671272
}
1273+
final gasMixRef = (tankData['uddfGasMixRef'] as String?)?.trim();
1274+
// First tank linked to a given gas wins; later tanks sharing the
1275+
// same gas don't overwrite. This is a pragmatic resolution for
1276+
// dives where multiple tanks carry the same mix.
1277+
if (gasMixRef != null &&
1278+
gasMixRef.isNotEmpty &&
1279+
!tankIdByGasMixRef.containsKey(gasMixRef)) {
1280+
tankIdByGasMixRef[gasMixRef] = tank.id;
1281+
}
12681282
}
12691283
}
12701284

@@ -1273,9 +1287,16 @@ class UddfEntityImporter {
12731287
final timestamp = gs['timestamp'] as int?;
12741288
if (timestamp == null) return null;
12751289
final tankRef = (gs['tankRef'] as String?)?.trim();
1276-
final tankId = tankRef != null && tankRef.isNotEmpty
1277-
? tankIdByRef[tankRef]
1278-
: null;
1290+
final gasMixRef = (gs['gasMixRef'] as String?)?.trim();
1291+
String? tankId;
1292+
if (tankRef != null && tankRef.isNotEmpty) {
1293+
tankId = tankIdByRef[tankRef];
1294+
}
1295+
if ((tankId == null || tankId.isEmpty) &&
1296+
gasMixRef != null &&
1297+
gasMixRef.isNotEmpty) {
1298+
tankId = tankIdByGasMixRef[gasMixRef];
1299+
}
12791300
if (tankId == null || tankId.isEmpty) return null;
12801301
return GasSwitch(
12811302
id: _uuid.v4(),

test/core/services/export/uddf/uddf_macdive_import_test.dart

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -761,18 +761,31 @@ void main() {
761761
});
762762

763763
test(
764-
'samples with <switchmix ref> record gasMixRef on the right sample',
764+
'emits gasSwitches from waypoint <switchmix ref> for multi-tank dives',
765765
() async {
766+
// MacDive deco-dive style: two tanks, each linked to a gas definition;
767+
// profile samples mark the switch via <switchmix ref="mix-deco"/>.
766768
const uddf = '''<?xml version="1.0" encoding="UTF-8" ?>
767769
<uddf xmlns="http://www.streit.cc/uddf/3.2/" version="3.2.1">
768770
<gasdefinitions>
769-
<mix id="mix-bottom"><o2>0.32</o2></mix>
770-
<mix id="mix-deco"><o2>0.80</o2></mix>
771+
<mix id="mix-bottom"><o2>0.32</o2><he>0.0</he></mix>
772+
<mix id="mix-deco"><o2>0.80</o2><he>0.0</he></mix>
771773
</gasdefinitions>
772774
<profiledata><repetitiongroup id="rg-1">
773775
<dive id="d-1">
774776
<informationbeforedive><datetime>2024-06-01T09:00:00</datetime></informationbeforedive>
775-
<informationafterdive><greatestdepth>40</greatestdepth><diveduration>3600</diveduration></informationafterdive>
777+
<informationafterdive>
778+
<greatestdepth>40</greatestdepth>
779+
<diveduration>3600</diveduration>
780+
</informationafterdive>
781+
<tankdata>
782+
<link ref="mix-bottom" />
783+
<tankvolume>0.012</tankvolume>
784+
</tankdata>
785+
<tankdata>
786+
<link ref="mix-deco" />
787+
<tankvolume>0.007</tankvolume>
788+
</tankdata>
776789
<samples>
777790
<waypoint><divetime>0</divetime><depth>0</depth><switchmix ref="mix-bottom"/></waypoint>
778791
<waypoint><divetime>120</divetime><depth>30</depth></waypoint>
@@ -782,17 +795,20 @@ void main() {
782795
</repetitiongroup></profiledata>
783796
</uddf>''';
784797
final r = await service.importAllDataFromUddf(uddf);
785-
final profile = r.dives.first['profile'] as List<Map<String, dynamic>>;
786-
expect(profile.length, 3);
787-
final switches = profile
788-
.where((p) => p['gasMixRef'] != null)
789-
.map((p) => p['gasMixRef'])
790-
.toList();
798+
final dive = r.dives.first;
799+
final switches =
800+
(dive['gasSwitches'] as List?)?.cast<Map<String, dynamic>>() ??
801+
const [];
791802
expect(
792-
switches,
793-
['mix-bottom', 'mix-deco'],
794-
reason: 'only samples with <switchmix ref> should have gasMixRef',
803+
switches.length,
804+
2,
805+
reason: 'two waypoints carry <switchmix ref>',
795806
);
807+
expect(switches[0]['timestamp'], 0);
808+
expect(switches[0]['gasMixRef'], 'mix-bottom');
809+
expect(switches[1]['timestamp'], 2400);
810+
expect(switches[1]['gasMixRef'], 'mix-deco');
811+
expect(switches[1]['depth'], 6);
796812
},
797813
);
798814
});

test/core/services/export/uddf/uddf_macdive_real_sample_test.dart

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -108,20 +108,24 @@ void main() {
108108
);
109109
});
110110

111-
test('at least one dive has gas-switch markers', () async {
112-
if (skipIfNoFixture()) return;
113-
final result = await service.importAllDataFromUddf(content);
114-
final withSwitch = result.dives.where((d) {
115-
final profile = d['profile'] as List?;
116-
if (profile == null) return false;
117-
return profile.any((p) => (p as Map)['gasMixRef'] != null);
118-
});
119-
expect(
120-
withSwitch,
121-
isNotEmpty,
122-
reason: 'sample contains multi-gas dives with <switchmix ref>',
123-
);
124-
});
111+
test(
112+
'at least one dive has gasSwitches entries from waypoint switchmix',
113+
() async {
114+
if (skipIfNoFixture()) return;
115+
final result = await service.importAllDataFromUddf(content);
116+
final withSwitches = result.dives.where((d) {
117+
final switches = d['gasSwitches'] as List?;
118+
return switches != null && switches.isNotEmpty;
119+
});
120+
expect(
121+
withSwitches,
122+
isNotEmpty,
123+
reason:
124+
'sample contains multi-gas deco dives marked via <switchmix ref> '
125+
'on waypoints; parser should emit those to diveData["gasSwitches"]',
126+
);
127+
},
128+
);
125129

126130
test('at least one site has country populated', () async {
127131
if (skipIfNoFixture()) return;

0 commit comments

Comments
 (0)