Skip to content

Commit c1c2d20

Browse files
committed
feat(uddf): persist MacDive waypoint gas switches via existing gasSwitches pipe
MacDive marks gas changes using <switchmix ref="gas-UUID"/> inside individual waypoint samples. The parser previously recorded this on the sample map (point['gasMixRef']) but it went nowhere; the importer resolves gas switches via tankRef, but MacDive's switchmix ref is a gas-mix UUID, not a tank UUID. Parser now emits gas switches into diveData['gasSwitches'] with gasMixRef (alongside the existing tankRef-from-top-level-<gasswitches> path), deduping timestamp|gasMixRef|tankRef so both sources feed one consumer. Tanks also record uddfGasMixRef so the downstream importer can resolve gas-mix UUID -> tank id. Importer now resolves both tankRef and gasMixRef to tankId, so single-pipe persistence works for both source-level and waypoint-level switch markers. Adds unit test for parser emission, integration test verifying gas_switches rows land on the correct dive_tanks rows, and updates the MacDive real-sample regression to assert gasSwitches entries rather than the now-removed sample-level gasMixRef. Closes the parser-to-DB gap noted as a known limitation in M1.
1 parent e3af9fd commit c1c2d20

5 files changed

Lines changed: 310 additions & 34 deletions

File tree

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

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1373,8 +1373,16 @@ class UddfFullImportService {
13731373
final mixLink = tankDataElement.findElements('link').firstOrNull;
13741374
if (mixLink != null) {
13751375
final mixRef = mixLink.getAttribute('ref');
1376-
if (mixRef != null && gasMixes.containsKey(mixRef)) {
1377-
tankInfo['gasMix'] = gasMixes[mixRef];
1376+
if (mixRef != null) {
1377+
// Record the UDDF gas-mix UUID on the tank so the importer can
1378+
// resolve waypoint-level <switchmix ref> markers (which reference
1379+
// gas mixes, not tanks) back to a tank for the gas_switches row.
1380+
if (mixRef.isNotEmpty) {
1381+
tankInfo['uddfGasMixRef'] = mixRef;
1382+
}
1383+
if (gasMixes.containsKey(mixRef)) {
1384+
tankInfo['gasMix'] = gasMixes[mixRef];
1385+
}
13781386
}
13791387
}
13801388

@@ -1528,6 +1536,11 @@ class UddfFullImportService {
15281536
final samplesElement = diveElement.findElements('samples').firstOrNull;
15291537
if (samplesElement != null) {
15301538
final profile = <Map<String, dynamic>>[];
1539+
// Gas switches emitted from waypoint-level <switchmix ref="..."/>.
1540+
// MacDive marks deco gas changes on individual samples this way; we feed
1541+
// them into the same `diveData['gasSwitches']` pipe as the top-level
1542+
// <gasswitches> section so the importer has one consumer.
1543+
final waypointGasSwitches = <Map<String, dynamic>>[];
15311544
GasMix? currentMix;
15321545
GasMix? pendingSwitchMix;
15331546
double? lastWaypointCns;
@@ -1565,8 +1578,22 @@ class UddfFullImportService {
15651578
if (switchMix != null) {
15661579
final mixRef = switchMix.getAttribute('ref');
15671580
if (mixRef != null) {
1568-
// Record the gas mix reference on the sample for downstream consumers
1569-
point['gasMixRef'] = mixRef;
1581+
// Emit a gas switch entry for the importer to persist. Shape
1582+
// matches the top-level <gasswitches> parser (timestamp/depth/
1583+
// tankRef), plus `gasMixRef` so the importer can resolve the
1584+
// MacDive-style gas-UUID reference to a tank.
1585+
final timestamp = point['timestamp'] as int?;
1586+
if (timestamp != null) {
1587+
final entry = <String, dynamic>{
1588+
'timestamp': timestamp,
1589+
'gasMixRef': mixRef,
1590+
};
1591+
final depth = point['depth'];
1592+
if (depth != null) {
1593+
entry['depth'] = depth;
1594+
}
1595+
waypointGasSwitches.add(entry);
1596+
}
15701597

15711598
if (gasMixes.containsKey(mixRef)) {
15721599
currentMix = gasMixes[mixRef];
@@ -1733,6 +1760,24 @@ class UddfFullImportService {
17331760
if (currentMix != null && !diveData.containsKey('tanks')) {
17341761
diveData['gasMix'] = currentMix;
17351762
}
1763+
1764+
// Merge waypoint-level gas switches with any entries emitted earlier
1765+
// from the top-level <gasswitches> section, deduping on
1766+
// timestamp+gasMixRef+tankRef so both paths feed one consumer.
1767+
if (waypointGasSwitches.isNotEmpty) {
1768+
final existing =
1769+
(diveData['gasSwitches'] as List<Map<String, dynamic>>?) ??
1770+
const <Map<String, dynamic>>[];
1771+
final seen = <String>{};
1772+
final merged = <Map<String, dynamic>>[];
1773+
for (final gs in [...existing, ...waypointGasSwitches]) {
1774+
final key = '${gs['timestamp']}|${gs['gasMixRef']}|${gs['tankRef']}';
1775+
if (seen.add(key)) {
1776+
merged.add(gs);
1777+
}
1778+
}
1779+
diveData['gasSwitches'] = merged;
1780+
}
17361781
}
17371782

17381783
// 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
@@ -1250,7 +1250,12 @@ class UddfEntityImporter {
12501250
final gasSwitchesData =
12511251
diveData['gasSwitches'] as List<Map<String, dynamic>>?;
12521252
if (gasSwitchesData != null && gasSwitchesData.isNotEmpty) {
1253+
// Build lookups from both UDDF tank ID and UDDF gas mix UUID to the
1254+
// persisted tank row id. MacDive-style switches reference a gas mix
1255+
// UUID (via <switchmix ref>), while top-level <gasswitches>
1256+
// entries reference a tank UUID (via <tankref>); we accept either.
12531257
final tankIdByRef = <String, String>{};
1258+
final tankIdByGasMixRef = <String, String>{};
12541259
final tanksData = diveData['tanks'] as List<Map<String, dynamic>>?;
12551260
if (tanksData != null) {
12561261
for (var i = 0; i < tanks.length && i < tanksData.length; i++) {
@@ -1260,6 +1265,15 @@ class UddfEntityImporter {
12601265
if (ref != null && ref.isNotEmpty) {
12611266
tankIdByRef[ref] = tank.id;
12621267
}
1268+
final gasMixRef = (tankData['uddfGasMixRef'] as String?)?.trim();
1269+
// First tank linked to a given gas wins; later tanks sharing the
1270+
// same gas don't overwrite. This is a pragmatic resolution for
1271+
// dives where multiple tanks carry the same mix.
1272+
if (gasMixRef != null &&
1273+
gasMixRef.isNotEmpty &&
1274+
!tankIdByGasMixRef.containsKey(gasMixRef)) {
1275+
tankIdByGasMixRef[gasMixRef] = tank.id;
1276+
}
12631277
}
12641278
}
12651279

@@ -1268,9 +1282,16 @@ class UddfEntityImporter {
12681282
final timestamp = gs['timestamp'] as int?;
12691283
if (timestamp == null) return null;
12701284
final tankRef = (gs['tankRef'] as String?)?.trim();
1271-
final tankId = tankRef != null && tankRef.isNotEmpty
1272-
? tankIdByRef[tankRef]
1273-
: null;
1285+
final gasMixRef = (gs['gasMixRef'] as String?)?.trim();
1286+
String? tankId;
1287+
if (tankRef != null && tankRef.isNotEmpty) {
1288+
tankId = tankIdByRef[tankRef];
1289+
}
1290+
if ((tankId == null || tankId.isEmpty) &&
1291+
gasMixRef != null &&
1292+
gasMixRef.isNotEmpty) {
1293+
tankId = tankIdByGasMixRef[gasMixRef];
1294+
}
12741295
if (tankId == null || tankId.isEmpty) return null;
12751296
return GasSwitch(
12761297
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
@@ -763,18 +763,31 @@ void main() {
763763
});
764764

765765
test(
766-
'samples with <switchmix ref> record gasMixRef on the right sample',
766+
'emits gasSwitches from waypoint <switchmix ref> for multi-tank dives',
767767
() async {
768+
// MacDive deco-dive style: two tanks, each linked to a gas definition;
769+
// profile samples mark the switch via <switchmix ref="mix-deco"/>.
768770
const uddf = '''<?xml version="1.0" encoding="UTF-8" ?>
769771
<uddf xmlns="http://www.streit.cc/uddf/3.2/" version="3.2.1">
770772
<gasdefinitions>
771-
<mix id="mix-bottom"><o2>0.32</o2></mix>
772-
<mix id="mix-deco"><o2>0.80</o2></mix>
773+
<mix id="mix-bottom"><o2>0.32</o2><he>0.0</he></mix>
774+
<mix id="mix-deco"><o2>0.80</o2><he>0.0</he></mix>
773775
</gasdefinitions>
774776
<profiledata><repetitiongroup id="rg-1">
775777
<dive id="d-1">
776778
<informationbeforedive><datetime>2024-06-01T09:00:00</datetime></informationbeforedive>
777-
<informationafterdive><greatestdepth>40</greatestdepth><diveduration>3600</diveduration></informationafterdive>
779+
<informationafterdive>
780+
<greatestdepth>40</greatestdepth>
781+
<diveduration>3600</diveduration>
782+
</informationafterdive>
783+
<tankdata>
784+
<link ref="mix-bottom" />
785+
<tankvolume>0.012</tankvolume>
786+
</tankdata>
787+
<tankdata>
788+
<link ref="mix-deco" />
789+
<tankvolume>0.007</tankvolume>
790+
</tankdata>
778791
<samples>
779792
<waypoint><divetime>0</divetime><depth>0</depth><switchmix ref="mix-bottom"/></waypoint>
780793
<waypoint><divetime>120</divetime><depth>30</depth></waypoint>
@@ -784,17 +797,20 @@ void main() {
784797
</repetitiongroup></profiledata>
785798
</uddf>''';
786799
final r = await service.importAllDataFromUddf(uddf);
787-
final profile = r.dives.first['profile'] as List<Map<String, dynamic>>;
788-
expect(profile.length, 3);
789-
final switches = profile
790-
.where((p) => p['gasMixRef'] != null)
791-
.map((p) => p['gasMixRef'])
792-
.toList();
800+
final dive = r.dives.first;
801+
final switches =
802+
(dive['gasSwitches'] as List?)?.cast<Map<String, dynamic>>() ??
803+
const [];
793804
expect(
794-
switches,
795-
['mix-bottom', 'mix-deco'],
796-
reason: 'only samples with <switchmix ref> should have gasMixRef',
805+
switches.length,
806+
2,
807+
reason: 'two waypoints carry <switchmix ref>',
797808
);
809+
expect(switches[0]['timestamp'], 0);
810+
expect(switches[0]['gasMixRef'], 'mix-bottom');
811+
expect(switches[1]['timestamp'], 2400);
812+
expect(switches[1]['gasMixRef'], 'mix-deco');
813+
expect(switches[1]['depth'], 6);
798814
},
799815
);
800816
});

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)