Files
maps-saas/packages/tactical_app/lib/services/dem_tile_elevation_service.dart
T

191 lines
5.9 KiB
Dart

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<String, DecodedDemTile> _memoryCache = {};
static Directory? _cacheDir;
static Future<Directory> _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<DecodedDemTile?> 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<void> prefetchTilesForCoords(List<LatLng> coords,
{int zoom = 12}) async {
final uniqueTiles = <String, ({int z, int x, int y})>{};
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<double> 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<double> getElevation(double lat, double lng, {int zoom = 12}) {
return getElevationAsync(lat, lng, zoom: zoom);
}
}