import 'dart:math' as math; import 'dart:typed_data'; import 'dart:ui' as ui; import 'package:http/http.dart' as http; import '../../domain/mission/terrain_math.dart'; import '../../domain/waypoint/flat_waypoint_list.dart'; /// Hoehendaten ueber AWS-Terrarium-Kacheln (Doku 3.9): PNG-kodierte /// Rasterkacheln im selben z/x/y-Schema wie die OSM-Basiskarte, jeder /// Pixel kodiert eine absolute Hoehe in den RGB-Kanaelen (Terrarium-Format: /// `elevation = R*256 + G + B/256 - 32768`). class TerrainService { TerrainService({http.Client? client}) : _client = client ?? http.Client(); final http.Client _client; static const _tileUrlTemplate = 'https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png'; static const _zoom = 13; static const _tileSize = 256; /// Kachelkoordinate plus Pixelposition innerhalb der Kachel fuer einen /// Punkt - analog lngLatToTilePixel() im HTML-Demonstrator. ({int tx, int ty, int px, int py}) _tilePixelFor(double lat, double lon) { final n = math.pow(2, _zoom).toDouble(); final x = (lon + 180) / 360 * n; final latRad = lat * math.pi / 180; final y = (1 - math.log(math.tan(latRad) + 1 / math.cos(latRad)) / math.pi) / 2 * n; final tx = x.floor(); final ty = y.floor(); final px = ((x - tx) * _tileSize).floor().clamp(0, _tileSize - 1); final py = ((y - ty) * _tileSize).floor().clamp(0, _tileSize - 1); return (tx: tx, ty: ty, px: px, py: py); } Future _fetchTileRgba(int tx, int ty) async { final url = _tileUrlTemplate .replaceAll('{z}', '$_zoom') .replaceAll('{x}', '$tx') .replaceAll('{y}', '$ty'); try { final response = await _client.get(Uri.parse(url)); if (response.statusCode != 200) return null; final codec = await ui.instantiateImageCodec(response.bodyBytes); final frame = await codec.getNextFrame(); final byteData = await frame.image.toByteData(format: ui.ImageByteFormat.rawRgba); return byteData?.buffer.asUint8List(); } catch (_) { // CORS-Aequivalent/Kachel fehlt/Decodierfehler: Punkt bleibt null, // kein Absturz (analog dem try/catch um ctx.getImageData() im // HTML-Demonstrator). return null; } } double? _elevationAt(Uint8List rgba, int px, int py) { final offset = (py * _tileSize + px) * 4; if (offset + 2 >= rgba.length) return null; final r = rgba[offset]; final g = rgba[offset + 1]; final b = rgba[offset + 2]; return (r * 256 + g + b / 256) - 32768; } /// Laedt das Gelaendeprofil fuer die aktuelle Route (Doku 3.9): sampelt /// distanzbasiert, gruppiert die Sample-Punkte nach Kachel (jede Kachel /// wird nur einmal geladen) und macht die Hoehe relativ zum ersten /// gueltigen Sample. Null, wenn keine einzige Kachel geladen werden /// konnte oder die Route weniger als zwei Wegpunkte hat. Future fetchProfile(List waypoints) async { if (waypoints.length < 2) return null; final numSamples = sampleCountFor(waypoints); final samples = sampleRouteByDistance(waypoints, numSamples); if (samples.isEmpty) return null; final tileGroups = >{}; final tileCoords = {}; final pixelForIndex = {}; for (var i = 0; i < samples.length; i++) { final t = _tilePixelFor(samples[i].point.latitude, samples[i].point.longitude); final key = '${t.tx}_${t.ty}'; tileGroups.putIfAbsent(key, () => []).add(i); tileCoords[key] = (t.tx, t.ty); pixelForIndex[i] = (t.px, t.py); } final elevations = List.filled(samples.length, null); await Future.wait(tileGroups.entries.map((entry) async { final (tx, ty) = tileCoords[entry.key]!; final rgba = await _fetchTileRgba(tx, ty); if (rgba == null) return; for (final idx in entry.value) { final (px, py) = pixelForIndex[idx]!; elevations[idx] = _elevationAt(rgba, px, py); } })); final validIdx = elevations.indexWhere((e) => e != null); if (validIdx == -1) return null; final home = elevations[validIdx]!; return TerrainProfile( routeKey: routeKeyForTerrain(waypoints), points: [ for (var i = 0; i < samples.length; i++) TerrainSamplePoint( distanceM: samples[i].distanceM, elevationRelM: elevations[i] != null ? elevations[i]! - home : null, ), ], ); } }