191 lines
5.9 KiB
Dart
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);
|
|
}
|
|
}
|