97 lines
3.4 KiB
Dart
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;
|
|
}
|
|
}
|