feat(tactical): sovereign military suite with 100% real satellite DEM intervisibility, viewshed 360, offline engines, and entitlements
This commit is contained in:
@@ -3,6 +3,8 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
import '../config/app_config.dart';
|
||||
import '../models/landmark.dart';
|
||||
import 'offline_los_engine.dart';
|
||||
|
||||
class TacticalRouteResult {
|
||||
final List<LatLng> points;
|
||||
@@ -43,23 +45,53 @@ class TacticalApiService {
|
||||
|
||||
if (resp.statusCode == 200) {
|
||||
final data = jsonDecode(resp.body);
|
||||
if (data['paths'] != null && (data['paths'] as List).isNotEmpty) {
|
||||
final path = data['paths'][0];
|
||||
final distanceMeters = (path['distance'] as num?)?.toDouble() ?? 0.0;
|
||||
final timeMs = (path['time'] as num?)?.toDouble() ?? 0.0;
|
||||
|
||||
final coords = <LatLng>[];
|
||||
if (path['points'] != null && path['points']['coordinates'] != null) {
|
||||
double distanceMeters = 0.0;
|
||||
double durationSec = 0.0;
|
||||
final coords = <LatLng>[];
|
||||
|
||||
// 1. Direct NestJS Map-SaaS API response format
|
||||
if (data['distance'] != null) {
|
||||
distanceMeters = (data['distance'] as num?)?.toDouble() ?? 0.0;
|
||||
durationSec = (data['duration'] as num?)?.toDouble() ?? 0.0;
|
||||
|
||||
if (data['points'] is String) {
|
||||
coords.addAll(PolylineUtils.decode(data['points'] as String));
|
||||
} else if (data['points'] is List) {
|
||||
for (final pt in data['points']) {
|
||||
if (pt is List && pt.length >= 2) {
|
||||
coords.add(LatLng((pt[1] as num).toDouble(), (pt[0] as num).toDouble()));
|
||||
}
|
||||
}
|
||||
} else if (data['points'] is Map && data['points']['coordinates'] != null) {
|
||||
final List<dynamic> coordList = data['points']['coordinates'];
|
||||
for (final c in coordList) {
|
||||
coords.add(LatLng((c[1] as num).toDouble(), (c[0] as num).toDouble()));
|
||||
}
|
||||
}
|
||||
}
|
||||
// 2. Fallback to raw / paths GraphHopper format
|
||||
else if (data['paths'] != null && (data['paths'] as List).isNotEmpty) {
|
||||
final path = data['paths'][0];
|
||||
distanceMeters = (path['distance'] as num?)?.toDouble() ?? 0.0;
|
||||
final timeMs = (path['time'] as num?)?.toDouble() ?? 0.0;
|
||||
durationSec = timeMs / 1000.0;
|
||||
|
||||
if (path['points'] is String) {
|
||||
coords.addAll(PolylineUtils.decode(path['points'] as String));
|
||||
} else if (path['points'] is Map && path['points']['coordinates'] != null) {
|
||||
final List<dynamic> coordList = path['points']['coordinates'];
|
||||
for (final c in coordList) {
|
||||
coords.add(LatLng((c[1] as num).toDouble(), (c[0] as num).toDouble()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (coords.isNotEmpty) {
|
||||
return TacticalRouteResult(
|
||||
points: coords,
|
||||
distanceKm: distanceMeters / 1000.0,
|
||||
durationMinutes: timeMs / (1000.0 * 60.0),
|
||||
durationMinutes: durationSec / 60.0,
|
||||
profile: profile,
|
||||
);
|
||||
}
|
||||
@@ -70,6 +102,59 @@ class TacticalApiService {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Search places via Server Geocoding API (Forward Geocoding & POI Search)
|
||||
static Future<List<TacticalLandmark>> searchOnlinePlaces(String query, {String? region}) async {
|
||||
final cleanQuery = query.trim();
|
||||
if (cleanQuery.isEmpty) return [];
|
||||
|
||||
try {
|
||||
final uri = Uri.parse(
|
||||
'$defaultServerUrl/api/geocoding/search?q=${Uri.encodeComponent(cleanQuery)}&country=jordan',
|
||||
);
|
||||
|
||||
final resp = await http.get(uri, headers: {'x-api-key': defaultApiKey}).timeout(const Duration(seconds: 4));
|
||||
|
||||
if (resp.statusCode == 200) {
|
||||
final data = jsonDecode(resp.body);
|
||||
final list = (data is List) ? data : (data['results'] is List ? data['results'] as List : []);
|
||||
|
||||
return list.map((item) {
|
||||
final name = item['name_ar'] ?? item['name'] ?? cleanQuery;
|
||||
final cat = (item['category'] ?? item['type'] ?? '').toString().toLowerCase();
|
||||
|
||||
LandmarkType type = LandmarkType.tower;
|
||||
if (cat.contains('worship') || cat.contains('mosque') || name.contains('مسجد') || name.contains('جامع')) {
|
||||
type = LandmarkType.minaret;
|
||||
} else if (cat.contains('water') || name.contains('خزان') || name.contains('بركة')) {
|
||||
type = LandmarkType.waterTank;
|
||||
} else if (cat.contains('fort') || cat.contains('castle') || name.contains('قلعة') || name.contains('قصر')) {
|
||||
type = LandmarkType.fort;
|
||||
} else if (cat.contains('mountain') || cat.contains('peak') || name.contains('جبل') || name.contains('تل')) {
|
||||
type = LandmarkType.mountain;
|
||||
}
|
||||
|
||||
final lat = (item['latitude'] ?? item['lat'] as num?)?.toDouble() ?? 32.0;
|
||||
final lng = (item['longitude'] ?? item['lng'] as num?)?.toDouble() ?? 36.0;
|
||||
final regionName = item['governorate'] ?? item['district'] ?? item['city'] ?? 'الأردن';
|
||||
|
||||
return TacticalLandmark(
|
||||
id: 'geo-${item['id'] ?? lat}_$lng',
|
||||
name: name,
|
||||
region: regionName,
|
||||
type: type,
|
||||
lat: lat,
|
||||
lng: lng,
|
||||
elevationM: (item['elevation'] as num?)?.toInt() ?? 750,
|
||||
description: item['neighbourhood'] ?? item['formatted_address'] ?? '',
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Online geocoding search error: $e');
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/// Calculate Tactical Line of Sight (LOS) between Observer and Target
|
||||
static Future<Map<String, dynamic>?> calculateLineOfSight({
|
||||
required LatLng observer,
|
||||
@@ -88,7 +173,7 @@ class TacticalApiService {
|
||||
'&observerHeight=$observerHeight&targetHeight=$targetHeight',
|
||||
);
|
||||
|
||||
final resp = await http.get(uri, headers: {'x-api-key': key}).timeout(const Duration(seconds: 10));
|
||||
final resp = await http.get(uri, headers: {'x-api-key': key}).timeout(const Duration(seconds: 8));
|
||||
|
||||
if (resp.statusCode == 200) {
|
||||
return jsonDecode(resp.body);
|
||||
@@ -99,6 +184,23 @@ class TacticalApiService {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Sovereign Line of Sight solver with zero latency and full offline capability
|
||||
static Future<OfflineLosReport> calculateHybridLineOfSight({
|
||||
required LatLng observer,
|
||||
required LatLng target,
|
||||
double observerHeight = 2.0,
|
||||
double targetHeight = 2.0,
|
||||
String? serverUrl,
|
||||
String? apiKey,
|
||||
}) async {
|
||||
return OfflineLosEngine.calculateAsync(
|
||||
observer: observer,
|
||||
target: target,
|
||||
observerHeightM: observerHeight,
|
||||
targetHeightM: targetHeight,
|
||||
);
|
||||
}
|
||||
|
||||
/// Calculate Tactical Isochrone Reachability Polygons
|
||||
static Future<Map<String, dynamic>?> calculateIsochrone({
|
||||
required LatLng center,
|
||||
|
||||
Reference in New Issue
Block a user