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
62 changes: 55 additions & 7 deletions lib/core/services/export/uddf/uddf_full_import_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1362,9 +1362,16 @@ class UddfFullImportService {
// Get linked gas mix
final mixLink = tankDataElement.findElements('link').firstOrNull;
if (mixLink != null) {
final mixRef = mixLink.getAttribute('ref');
if (mixRef != null && gasMixes.containsKey(mixRef)) {
tankInfo['gasMix'] = gasMixes[mixRef];
final rawMixRef = mixLink.getAttribute('ref');
final mixRef = rawMixRef?.trim();
if (mixRef != null && mixRef.isNotEmpty) {
// Record the UDDF gas-mix UUID on the tank so the importer can
// resolve waypoint-level <switchmix ref> markers (which reference
// gas mixes, not tanks) back to a tank for the gas_switches row.
tankInfo['uddfGasMixRef'] = mixRef;
if (gasMixes.containsKey(mixRef)) {
tankInfo['gasMix'] = gasMixes[mixRef];
Comment thread
ericgriffin marked this conversation as resolved.
}
}
}

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

final switchMix = waypoint.findElements('switchmix').firstOrNull;
if (switchMix != null) {
final mixRef = switchMix.getAttribute('ref');
if (mixRef != null) {
// Record the gas mix reference on the sample for downstream consumers
point['gasMixRef'] = mixRef;
final rawMixRef = switchMix.getAttribute('ref');
final mixRef = rawMixRef?.trim();
// Skip emission entirely when the ref is empty or whitespace-only:
// the importer would have no way to resolve such a dangling ref
// back to a persisted tank row.
if (mixRef != null && mixRef.isNotEmpty) {
// Emit a gas switch entry for the importer to persist. Shape
// matches the top-level <gasswitches> parser (timestamp/depth/
// tankRef), plus `gasMixRef` so the importer can resolve the
// MacDive-style gas-UUID reference to a tank.
final timestamp = point['timestamp'] as int?;
if (timestamp != null) {
final entry = <String, dynamic>{
'timestamp': timestamp,
'gasMixRef': mixRef,
};
Comment thread
ericgriffin marked this conversation as resolved.
final depth = point['depth'];
if (depth != null) {
entry['depth'] = depth;
}
waypointGasSwitches.add(entry);
}

if (gasMixes.containsKey(mixRef)) {
currentMix = gasMixes[mixRef];
Expand Down Expand Up @@ -1723,6 +1753,24 @@ class UddfFullImportService {
if (currentMix != null && !diveData.containsKey('tanks')) {
diveData['gasMix'] = currentMix;
}

// Merge waypoint-level gas switches with any entries emitted earlier
// from the top-level <gasswitches> section, deduping on
// timestamp+gasMixRef+tankRef so both paths feed one consumer.
if (waypointGasSwitches.isNotEmpty) {
final existing =
(diveData['gasSwitches'] as List<Map<String, dynamic>>?) ??
const <Map<String, dynamic>>[];
final seen = <String>{};
final merged = <Map<String, dynamic>>[];
for (final gs in [...existing, ...waypointGasSwitches]) {
final key = '${gs['timestamp']}|${gs['gasMixRef']}|${gs['tankRef']}';
if (seen.add(key)) {
merged.add(gs);
}
}
diveData['gasSwitches'] = merged;
}
}

// Parse information after dive
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1255,7 +1255,12 @@ class UddfEntityImporter {
final gasSwitchesData =
diveData['gasSwitches'] as List<Map<String, dynamic>>?;
if (gasSwitchesData != null && gasSwitchesData.isNotEmpty) {
// Build lookups from both UDDF tank ID and UDDF gas mix UUID to the
// persisted tank row id. MacDive-style switches reference a gas mix
// UUID (via <switchmix ref>), while top-level <gasswitches>
// entries reference a tank UUID (via <tankref>); we accept either.
final tankIdByRef = <String, String>{};
final tankIdByGasMixRef = <String, String>{};
final tanksData = diveData['tanks'] as List<Map<String, dynamic>>?;
if (tanksData != null) {
for (var i = 0; i < tanks.length && i < tanksData.length; i++) {
Expand All @@ -1265,6 +1270,15 @@ class UddfEntityImporter {
if (ref != null && ref.isNotEmpty) {
tankIdByRef[ref] = tank.id;
}
final gasMixRef = (tankData['uddfGasMixRef'] as String?)?.trim();
// First tank linked to a given gas wins; later tanks sharing the
// same gas don't overwrite. This is a pragmatic resolution for
// dives where multiple tanks carry the same mix.
if (gasMixRef != null &&
gasMixRef.isNotEmpty &&
!tankIdByGasMixRef.containsKey(gasMixRef)) {
tankIdByGasMixRef[gasMixRef] = tank.id;
}
}
}

Expand All @@ -1273,9 +1287,16 @@ class UddfEntityImporter {
final timestamp = gs['timestamp'] as int?;
if (timestamp == null) return null;
final tankRef = (gs['tankRef'] as String?)?.trim();
final tankId = tankRef != null && tankRef.isNotEmpty
? tankIdByRef[tankRef]
: null;
final gasMixRef = (gs['gasMixRef'] as String?)?.trim();
String? tankId;
if (tankRef != null && tankRef.isNotEmpty) {
tankId = tankIdByRef[tankRef];
}
if ((tankId == null || tankId.isEmpty) &&
gasMixRef != null &&
gasMixRef.isNotEmpty) {
tankId = tankIdByGasMixRef[gasMixRef];
}
if (tankId == null || tankId.isEmpty) return null;
return GasSwitch(
id: _uuid.v4(),
Expand Down
42 changes: 29 additions & 13 deletions test/core/services/export/uddf/uddf_macdive_import_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -761,18 +761,31 @@ void main() {
});

test(
'samples with <switchmix ref> record gasMixRef on the right sample',
'emits gasSwitches from waypoint <switchmix ref> for multi-tank dives',
() async {
// MacDive deco-dive style: two tanks, each linked to a gas definition;
// profile samples mark the switch via <switchmix ref="mix-deco"/>.
const uddf = '''<?xml version="1.0" encoding="UTF-8" ?>
<uddf xmlns="http://www.streit.cc/uddf/3.2/" version="3.2.1">
<gasdefinitions>
<mix id="mix-bottom"><o2>0.32</o2></mix>
<mix id="mix-deco"><o2>0.80</o2></mix>
<mix id="mix-bottom"><o2>0.32</o2><he>0.0</he></mix>
<mix id="mix-deco"><o2>0.80</o2><he>0.0</he></mix>
</gasdefinitions>
<profiledata><repetitiongroup id="rg-1">
<dive id="d-1">
<informationbeforedive><datetime>2024-06-01T09:00:00</datetime></informationbeforedive>
<informationafterdive><greatestdepth>40</greatestdepth><diveduration>3600</diveduration></informationafterdive>
<informationafterdive>
<greatestdepth>40</greatestdepth>
<diveduration>3600</diveduration>
</informationafterdive>
<tankdata>
<link ref="mix-bottom" />
<tankvolume>0.012</tankvolume>
</tankdata>
<tankdata>
<link ref="mix-deco" />
<tankvolume>0.007</tankvolume>
</tankdata>
<samples>
<waypoint><divetime>0</divetime><depth>0</depth><switchmix ref="mix-bottom"/></waypoint>
<waypoint><divetime>120</divetime><depth>30</depth></waypoint>
Expand All @@ -782,17 +795,20 @@ void main() {
</repetitiongroup></profiledata>
</uddf>''';
final r = await service.importAllDataFromUddf(uddf);
final profile = r.dives.first['profile'] as List<Map<String, dynamic>>;
expect(profile.length, 3);
final switches = profile
.where((p) => p['gasMixRef'] != null)
.map((p) => p['gasMixRef'])
.toList();
final dive = r.dives.first;
final switches =
(dive['gasSwitches'] as List?)?.cast<Map<String, dynamic>>() ??
const [];
expect(
switches,
['mix-bottom', 'mix-deco'],
reason: 'only samples with <switchmix ref> should have gasMixRef',
switches.length,
2,
reason: 'two waypoints carry <switchmix ref>',
);
expect(switches[0]['timestamp'], 0);
expect(switches[0]['gasMixRef'], 'mix-bottom');
expect(switches[1]['timestamp'], 2400);
expect(switches[1]['gasMixRef'], 'mix-deco');
expect(switches[1]['depth'], 6);
},
);
});
Expand Down
32 changes: 18 additions & 14 deletions test/core/services/export/uddf/uddf_macdive_real_sample_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -108,20 +108,24 @@ void main() {
);
});

test('at least one dive has gas-switch markers', () async {
if (skipIfNoFixture()) return;
final result = await service.importAllDataFromUddf(content);
final withSwitch = result.dives.where((d) {
final profile = d['profile'] as List?;
if (profile == null) return false;
return profile.any((p) => (p as Map)['gasMixRef'] != null);
});
expect(
withSwitch,
isNotEmpty,
reason: 'sample contains multi-gas dives with <switchmix ref>',
);
});
test(
'at least one dive has gasSwitches entries from waypoint switchmix',
() async {
if (skipIfNoFixture()) return;
final result = await service.importAllDataFromUddf(content);
final withSwitches = result.dives.where((d) {
final switches = d['gasSwitches'] as List?;
return switches != null && switches.isNotEmpty;
});
expect(
withSwitches,
isNotEmpty,
reason:
'sample contains multi-gas deco dives marked via <switchmix ref> '
'on waypoints; parser should emit those to diveData["gasSwitches"]',
);
},
);

test('at least one site has country populated', () async {
if (skipIfNoFixture()) return;
Expand Down
Loading
Loading