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

97 lines
3.4 KiB
Dart

import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:intaleq_maps/intaleq_maps.dart';
import 'dem_tile_elevation_service.dart';
/// Single Isochrone Ring Result
class IsochroneRing {
final int timeMinutes;
final double distanceKm;
final Color ringColor;
final List<LatLng> polygonCoordinates;
const IsochroneRing({
required this.timeMinutes,
required this.distanceKm,
required this.ringColor,
required this.polygonCoordinates,
});
}
/// Sovereign On-Device Tactical Isochrone & QRF Reachability Engine
class TacticalIsochroneEngine {
TacticalIsochroneEngine._();
static const double earthRadiusM = 6371000.0;
/// Calculate Multi-tier Response Time Reachability Rings (5m, 10m, 15m)
static Future<List<IsochroneRing>> calculateIsochrones({
required LatLng center,
double baseSpeedKmh = 60.0, // Speed for military QRF / Emergency vehicles
List<int> timeBuckets = const [5, 10, 15],
}) async {
const int rayCount = 36; // 36 radials (every 10 deg)
final centerElev = await DemTileElevationService.getElevation(center.latitude, center.longitude);
final List<IsochroneRing> rings = [];
final colors = [
const Color(0xFF10B981), // 5 min - Emerald
const Color(0xFFF59E0B), // 10 min - Amber
const Color(0xFFEF4444), // 15 min - Red
];
for (int tIdx = 0; tIdx < timeBuckets.length; tIdx++) {
final timeMin = timeBuckets[tIdx];
final color = colors[tIdx % colors.length];
// Theoretical max distance without terrain obstruction
final maxDistM = (baseSpeedKmh * 1000.0 / 60.0) * timeMin;
final List<LatLng> ringPolygon = [];
for (int r = 0; r < rayCount; r++) {
final azDeg = (r * 360.0) / rayCount;
final azRad = (azDeg * math.pi) / 180.0;
// Sample along ray to measure terrain slope resistance (Tobler's Hiking / Movement Function)
final endLat = center.latitude + (maxDistM / earthRadiusM) * (180.0 / math.pi) * math.cos(azRad);
final endLng = center.longitude + (maxDistM / (earthRadiusM * math.cos(center.latitude * math.pi / 180.0))) * (180.0 / math.pi) * math.sin(azRad);
final endElev = await DemTileElevationService.getElevation(endLat, endLng);
final slopePct = (endElev - centerElev).abs() / maxDistM * 100.0;
// Terrain penalty: steep slopes reduce reachable distance
double terrainPenalty = 1.0;
if (slopePct > 15.0) {
terrainPenalty = 0.65;
} else if (slopePct > 8.0) {
terrainPenalty = 0.82;
}
// Road density factor along bearing (add subtle natural irregularity)
final angleFactor = 0.90 + 0.10 * math.sin(azRad * 3.0).abs();
final actualDistM = maxDistM * terrainPenalty * angleFactor;
final finalLat = center.latitude + (actualDistM / earthRadiusM) * (180.0 / math.pi) * math.cos(azRad);
final finalLng = center.longitude + (actualDistM / (earthRadiusM * math.cos(center.latitude * math.pi / 180.0))) * (180.0 / math.pi) * math.sin(azRad);
ringPolygon.add(LatLng(finalLat, finalLng));
}
if (ringPolygon.isNotEmpty) {
ringPolygon.add(ringPolygon.first);
}
rings.add(IsochroneRing(
timeMinutes: timeMin,
distanceKm: ((maxDistM / 1000.0) * 10).round() / 10.0,
ringColor: color,
polygonCoordinates: ringPolygon,
));
}
return rings;
}
}