import 'dart:convert'; import 'package:drift/drift.dart'; import '../../domain/mission/terrain_math.dart'; import '../../domain/waypoint/flat_waypoint_list.dart'; import 'app_database.dart'; import 'terrain_json_codec.dart'; import 'waypoint_json_codec.dart'; /// Kopfzeilen-taugliche Zusammenfassung einer gespeicherten Mission, ohne /// die komplette Wegpunktliste zu materialisieren (fuer die Missionsliste /// im Verwaltungsmenue, Doku 3.6/7.1). class MissionSummary { const MissionSummary({ required this.id, required this.name, required this.placeName, required this.waypointCount, required this.updatedAt, }); final int id; final String name; final String? placeName; final int waypointCount; final DateTime updatedAt; } class LoadedMission { const LoadedMission({ required this.name, required 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 zuletzt /// gespeicherte Route bereits eins abgerufen wurde - null, wenn noch /// keins vorliegt oder es (Route inzwischen anders) verworfen werden muss. final TerrainProfile? terrainProfile; } /// Persistenz-Zugriff fuer Missionen (Doku 4.15): kapselt die Drift-Tabelle /// und die JSON-(De-)Serialisierung der Wegpunktliste - die Domain-Schicht /// (Waypoint) kennt bewusst kein JSON, siehe flat_waypoint_list.dart. class MissionRepository { MissionRepository(this._db); final AppDatabase _db; Stream> watchSummaries() { return _db.select(_db.missions).watch().map((rows) { final summaries = [ for (final row in rows) MissionSummary( id: row.id, name: row.name, placeName: row.location, waypointCount: (jsonDecode(row.waypointsJson) as List).length, updatedAt: row.updatedAt, ), ]; return summaries; }); } Future load(int id) async { final row = await (_db.select(_db.missions) ..where((m) => m.id.equals(id))) .getSingleOrNull(); if (row == null) return null; final decoded = jsonDecode(row.waypointsJson) as List; return LoadedMission( name: row.name, placeName: row.location, createdAt: row.createdAt, waypoints: [ for (final entry in decoded) waypointFromJson(entry as Map), ], terrainProfile: row.terrainProfileJson != null ? decodeTerrainProfile(row.terrainProfileJson!) : null, ); } /// Legt eine neue Mission an oder ueberschreibt die bestehende mit [id] - /// analog saveCurrentMission()/Mission.toRecord() im HTML-Demonstrator /// (find-or-insert nach id). Gibt die (ggf. neu vergebene) id zurueck. Future upsert({ int? id, required String name, String? placeName, required List waypoints, }) async { final waypointsJson = jsonEncode([for (final w in waypoints) waypointToJson(w)]); final companion = MissionsCompanion( name: Value(name), location: Value(placeName), updatedAt: Value(DateTime.now()), waypointsJson: Value(waypointsJson), ); if (id == null) { return _db.into(_db.missions).insert(companion); } final existing = await (_db.select(_db.missions) ..where((m) => m.id.equals(id))) .getSingleOrNull(); if (existing == null) { return _db.into(_db.missions).insert(companion.copyWith(id: Value(id))); } await (_db.update(_db.missions)..where((m) => m.id.equals(id))) .write(companion); return id; } /// Persistiert ein frisch abgerufenes Bodenhoehenprofil fuer eine bereits /// gespeicherte Mission (Doku 3.9) - separat von [upsert], damit ein /// gewoehnlicher Wegpunkt-Autosave (bei dem noch kein Profil vorliegt) /// nicht versehentlich ein zuvor gespeichertes Profil loescht. Future saveTerrainProfile(int id, TerrainProfile profile) async { await (_db.update(_db.missions)..where((m) => m.id.equals(id))).write( MissionsCompanion(terrainProfileJson: Value(encodeTerrainProfile(profile))), ); } Future rename(int id, String name) async { await (_db.update(_db.missions)..where((m) => m.id.equals(id))).write( MissionsCompanion(name: Value(name), updatedAt: Value(DateTime.now())), ); } Future delete(int id) async { await (_db.delete(_db.missions)..where((m) => m.id.equals(id))).go(); } }