import 'dart:io'; import 'dart:math' as math; import 'dart:ui' as ui; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import 'package:intaleq_maps/intaleq_maps.dart'; import 'package:path_provider/path_provider.dart'; import 'offline_los_engine.dart'; class DecodedDemTile { final int zoom; final int x; final int y; final ByteData rgbaBytes; final int width; final int height; DecodedDemTile({ required this.zoom, required this.x, required this.y, required this.rgbaBytes, this.width = 256, this.height = 256, }); /// Sample sub-pixel elevation from Terrarium DEM tile using Bilinear Interpolation double sampleElevation(double subX, double subY) { final clampedX = subX.clamp(0.0, 254.99); final clampedY = subY.clamp(0.0, 254.99); final x0 = clampedX.floor(); final x1 = x0 + 1; final y0 = clampedY.floor(); final y1 = y0 + 1; final fx = clampedX - x0; final fy = clampedY - y0; double decodePixel(int px, int py) { final idx = (py * width + px) * 4; if (idx + 3 >= rgbaBytes.lengthInBytes) return 750.0; final r = rgbaBytes.getUint8(idx); final g = rgbaBytes.getUint8(idx + 1); final b = rgbaBytes.getUint8(idx + 2); // Terrarium formula: (R * 256 + G + B / 256) - 32768 return (r * 256.0 + g.toDouble() + b.toDouble() / 256.0) - 32768.0; } final z00 = decodePixel(x0, y0); final z10 = decodePixel(x1, y0); final z01 = decodePixel(x0, y1); final z11 = decodePixel(x1, y1); final zTop = z00 * (1.0 - fx) + z10 * fx; final zBottom = z01 * (1.0 - fx) + z11 * fx; return (zTop * (1.0 - fy) + zBottom * fy).roundToDouble(); } } /// Sovereign Satellite DEM Elevation Service with On-Device Disk Caching class DemTileElevationService { DemTileElevationService._(); static final Map _memoryCache = {}; static Directory? _cacheDir; static Future _getCacheDirectory() async { if (_cacheDir != null) return _cacheDir!; final appDir = await getApplicationDocumentsDirectory(); final demDir = Directory('${appDir.path}/dem_tiles'); if (!await demDir.exists()) { await demDir.create(recursive: true); } _cacheDir = demDir; return _cacheDir!; } /// Convert Lat/Lng to tile X, Y and Sub-pixel offsets at specified zoom level static ({int tileX, int tileY, double subX, double subY}) _coordToTile( double lat, double lng, int zoom) { final n = 1 << zoom; final xVal = ((lng + 180.0) / 360.0) * n; final latRad = lat * (math.pi / 180.0); final yVal = (1.0 - math.log(math.tan(latRad) + 1.0 / math.cos(latRad)) / math.pi) / 2.0 * n; final tileX = xVal.floor(); final tileY = yVal.floor(); final subX = (xVal - tileX) * 256.0; final subY = (yVal - tileY) * 256.0; return (tileX: tileX, tileY: tileY, subX: subX, subY: subY); } /// Get or fetch a single DEM tile with disk persistence static Future getTile(int zoom, int x, int y) async { final key = '$zoom/$x/$y'; if (_memoryCache.containsKey(key)) { return _memoryCache[key]; } try { final cacheDir = await _getCacheDirectory(); final localFile = File('${cacheDir.path}/${zoom}_${x}_$y.png'); Uint8List bytes; if (await localFile.exists()) { bytes = await localFile.readAsBytes(); } else { final url = Uri.parse( 'https://s3.amazonaws.com/elevation-tiles-prod/terrarium/$zoom/$x/$y.png'); final resp = await http.get(url).timeout(const Duration(seconds: 4)); if (resp.statusCode == 200) { bytes = resp.bodyBytes; await localFile.writeAsBytes(bytes); } else { return null; } } final codec = await ui.instantiateImageCodec(bytes); final frame = await codec.getNextFrame(); final image = frame.image; final byteData = await image.toByteData(format: ui.ImageByteFormat.rawRgba); if (byteData != null) { final decoded = DecodedDemTile( zoom: zoom, x: x, y: y, rgbaBytes: byteData, width: image.width, height: image.height, ); _memoryCache[key] = decoded; return decoded; } } catch (e) { debugPrint('DemTileElevationService: Failed to load tile $key: $e'); } return null; } /// Pre-fetch all DEM tiles needed for a bounding box or list of coordinates static Future prefetchTilesForCoords(List coords, {int zoom = 12}) async { final uniqueTiles = {}; for (final pt in coords) { final t = _coordToTile(pt.latitude, pt.longitude, zoom); final k = '$zoom/${t.tileX}/${t.tileY}'; uniqueTiles[k] = (z: zoom, x: t.tileX, y: t.tileY); } await Future.wait( uniqueTiles.values.map((t) => getTile(t.z, t.x, t.y)), ); } /// Synchronously get elevation if tile is cached in memory, otherwise fallback to Jordan DEM Surface static double getElevationSync(double lat, double lng, {int zoom = 12}) { final t = _coordToTile(lat, lng, zoom); final key = '$zoom/${t.tileX}/${t.tileY}'; final tile = _memoryCache[key]; if (tile != null) { return tile.sampleElevation(t.subX, t.subY); } return JordanDemSurface.elevationAt(lat, lng); } /// Asynchronously get real satellite elevation with on-demand tile loading static Future getElevationAsync(double lat, double lng, {int zoom = 12}) async { final t = _coordToTile(lat, lng, zoom); final tile = await getTile(zoom, t.tileX, t.tileY); if (tile != null) { return tile.sampleElevation(t.subX, t.subY); } return JordanDemSurface.elevationAt(lat, lng); } /// Standard alias for asynchronous elevation query static Future getElevation(double lat, double lng, {int zoom = 12}) { return getElevationAsync(lat, lng, zoom: zoom); } }