import 'dart:convert'; import '../../app_info.dart'; import '../../domain/mission/terrain_math.dart'; import '../../domain/waypoint/flat_waypoint_list.dart'; import '../database/terrain_json_codec.dart'; import '../database/waypoint_json_codec.dart'; /// Missionsdaten fuer den Export - unabhaengig von der Drift-Zeilenklasse, /// damit dieser Codec ohne Datenbankzugriff testbar bleibt. class MissionExportData { const MissionExportData({ required this.name, this.placeName, required this.createdAt, required this.waypoints, this.terrainProfile, }); final String name; final String? placeName; final DateTime createdAt; final List waypoints; /// Gecachtes Bodenhoehenprofil (Doku 3.9), falls fuer die Mission bereits /// eins abgerufen wurde - wird mitexportiert, damit ein Import auf einem /// anderen Geraet nicht erneut alle Kacheln laden muss. final TerrainProfile? terrainProfile; } /// Baut die Export-Datei fuer eine Mission (Doku 3.6, analog /// buildMissionExportObject()/shareMission() im HTML-Demonstrator) - ein /// mit `appVersion`/`exportedAt` umschlossenes JSON-Dokument, damit ein /// Import spaeter erkennen kann, aus welcher App-Version die Datei stammt. String buildMissionExportJson(MissionExportData mission) { final map = { 'appVersion': kAppVersion, 'exportedAt': DateTime.now().toUtc().toIso8601String(), 'mission': { 'name': mission.name, 'placeName': mission.placeName, 'createdAt': mission.createdAt.toUtc().toIso8601String(), 'waypoints': [for (final w in mission.waypoints) waypointToJson(w)], if (mission.terrainProfile != null) 'terrainProfile': terrainProfileToJson(mission.terrainProfile!), }, }; return const JsonEncoder.withIndent(' ').convert(map); } /// Vorschlagsname fuer die geteilte Datei (Doku 3.6, analog /// `'mission-' + safeName + '.json'` im HTML-Demonstrator). String missionExportFilename(String label) { final safeName = label.replaceAll(RegExp(r'[^a-zA-Z0-9]'), '_'); return 'mission-$safeName.json'; } class ParsedMissionImport { const ParsedMissionImport({ required this.name, this.placeName, required this.waypoints, this.versionMismatch, this.terrainProfile, }); final String name; final String? placeName; final List waypoints; /// Enthaelt die appVersion der Datei, falls sie sich von [kAppVersion] in /// der Hauptversionsnummer unterscheidet - sonst null (analog der /// "Imported from a different app version"-Warnung im HTML-Demonstrator). final String? versionMismatch; /// Mitexportiertes Bodenhoehenprofil (Doku 3.9), falls die Datei eins /// enthaelt - null bei Dateien aus einer Version ohne dieses Feld oder /// wenn die Quellmission noch keins hatte. final TerrainProfile? terrainProfile; } /// Parst eine importierte Missionsdatei - wirft [FormatException] bei /// ungueltigem JSON oder fehlenden Wegpunkten (Doku 3.6, analog dem /// `change`-Handler von importMissionInput im HTML-Demonstrator). ParsedMissionImport parseMissionImportJson(String jsonText) { final Object? decoded; try { decoded = jsonDecode(jsonText); } on FormatException { throw const FormatException('Invalid mission file'); } if (decoded is! Map) { throw const FormatException('Invalid mission file'); } final missionData = decoded['mission'] ?? decoded; if (missionData is! Map) { throw const FormatException('Invalid mission file'); } final waypointsRaw = missionData['waypoints']; if (waypointsRaw is! List || waypointsRaw.isEmpty) { throw const FormatException('File has no waypoints'); } final waypoints = [ for (final w in waypointsRaw) waypointFromJson(w as Map), ]; final name = (missionData['name'] as String?) ?? (missionData['placeName'] as String?) ?? 'Imported mission'; final placeName = (missionData['placeName'] as String?) ?? (missionData['name'] as String?); final appVersion = decoded['appVersion'] as String?; final mismatch = appVersion != null && appVersion.split('.').first != kAppVersion.split('.').first ? appVersion : null; final terrainProfileRaw = missionData['terrainProfile']; final terrainProfile = terrainProfileRaw is Map ? terrainProfileFromJson(terrainProfileRaw) : null; return ParsedMissionImport( name: name, placeName: placeName, waypoints: waypoints, versionMismatch: mismatch, terrainProfile: terrainProfile, ); }