The Mini-Altitude-Profile terrain data only ever lived in TerrainNotifier's in-memory state (one slot, keyed by route). Every app restart, or every switch away from and back to a mission, forced a full re-fetch of all AWS Terrarium elevation tiles for the route - the noticeably slow load the user reported for some missions was this happening on every visit, not just once. Adds a nullable terrainProfileJson column to the missions table (schema v6->v7) and a saveTerrainProfile() repository method, kept separate from the regular waypoint upsert() so an ordinary autosave never clobbers an already-cached profile. TerrainNotifier persists a profile right after a successful fetch (fire-and-forget) and gains restore(), called from CurrentMissionMetaNotifier whenever a mission is loaded/ started so a previously fetched profile is available immediately - ensureFor() still validates its routeKey before use, so a stale restored profile is never shown for a route that has since changed. Also included in mission export/import (MissionExportData/ ParsedMissionImport) so sharing a mission carries its terrain cache along instead of forcing the recipient to refetch it. Sample-count/resolution stays as-is for now (still fixed 30m spacing, 10-2000 samples) - adapting the sampling density to terrain variance (e.g. coarser sampling over flat ground) is a separate follow-up. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
132 lines
4.5 KiB
Dart
132 lines
4.5 KiB
Dart
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<Waypoint> 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<Waypoint> 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<String, dynamic>) {
|
|
throw const FormatException('Invalid mission file');
|
|
}
|
|
final missionData = decoded['mission'] ?? decoded;
|
|
if (missionData is! Map<String, dynamic>) {
|
|
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<String, dynamic>),
|
|
];
|
|
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<String, dynamic>
|
|
? terrainProfileFromJson(terrainProfileRaw)
|
|
: null;
|
|
|
|
return ParsedMissionImport(
|
|
name: name,
|
|
placeName: placeName,
|
|
waypoints: waypoints,
|
|
versionMismatch: mismatch,
|
|
terrainProfile: terrainProfile,
|
|
);
|
|
}
|