feat(tactical): sovereign military suite with 100% real satellite DEM intervisibility, viewshed 360, offline engines, and entitlements
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
import 'dart:io';
|
||||
import 'dart:math' as math;
|
||||
import 'dart:typed_data';
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user