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 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> calculateIsochrones({ required LatLng center, double baseSpeedKmh = 60.0, // Speed for military QRF / Emergency vehicles List 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 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 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; } }