308 lines
11 KiB
Dart
308 lines
11 KiB
Dart
import 'dart:math' as math;
|
|
import 'package:intaleq_maps/intaleq_maps.dart';
|
|
|
|
/// Military Coordinates Utility (WGS84 <-> UTM Zone 36N & MGRS Grid)
|
|
/// Specifically optimized for Jordan & Levantine military operations (Zone 36N / 37N)
|
|
class MilitaryGridUtils {
|
|
MilitaryGridUtils._();
|
|
|
|
static const double _a = 6378137.0; // WGS84 semi-major axis
|
|
static const double _f = 1 / 298.257223563; // WGS84 flattening
|
|
static const double _b = _a * (1.0 - _f);
|
|
static const double _eSq = (_a * _a - _b * _b) / (_a * _a);
|
|
static const double _ePrimeSq = (_a * _a - _b * _b) / (_b * _b);
|
|
static const double _k0 = 0.9996; // UTM scale factor
|
|
static const double earthRadiusM = 6371000.0;
|
|
|
|
/// Haversine Great Circle Distance in meters
|
|
static double haversineDistance(double lat1, double lng1, double lat2, double lng2) {
|
|
final dLat = (lat2 - lat1) * (math.pi / 180.0);
|
|
final dLng = (lng2 - lng1) * (math.pi / 180.0);
|
|
final a = math.sin(dLat / 2.0) * math.sin(dLat / 2.0) +
|
|
math.cos(lat1 * math.pi / 180.0) *
|
|
math.cos(lat2 * math.pi / 180.0) *
|
|
math.sin(dLng / 2.0) *
|
|
math.sin(dLng / 2.0);
|
|
final c = 2.0 * math.atan2(math.sqrt(a), math.sqrt(1.0 - a));
|
|
return earthRadiusM * c;
|
|
}
|
|
|
|
/// Initial Great Circle Bearing in degrees (0..360)
|
|
static double calculateBearing(double lat1, double lng1, double lat2, double lng2) {
|
|
final phi1 = lat1 * (math.pi / 180.0);
|
|
final phi2 = lat2 * (math.pi / 180.0);
|
|
final deltaLambda = (lng2 - lng1) * (math.pi / 180.0);
|
|
|
|
final y = math.sin(deltaLambda) * math.cos(phi2);
|
|
final x = math.cos(phi1) * math.sin(phi2) -
|
|
math.sin(phi1) * math.cos(phi2) * math.cos(deltaLambda);
|
|
final theta = math.atan2(y, x);
|
|
return (theta * (180.0 / math.pi) + 360.0) % 360.0;
|
|
}
|
|
|
|
/// Convert Azimuth Degrees to Arabic Cardinal Name
|
|
static String azimuthToCardinalArabic(double azimuthDeg) {
|
|
final deg = (azimuthDeg % 360.0 + 360.0) % 360.0;
|
|
if (deg >= 337.5 || deg < 22.5) return 'شمال (N)';
|
|
if (deg >= 22.5 && deg < 67.5) return 'شمال شرق (NE)';
|
|
if (deg >= 67.5 && deg < 112.5) return 'شرق (E)';
|
|
if (deg >= 112.5 && deg < 157.5) return 'جنوب شرق (SE)';
|
|
if (deg >= 157.5 && deg < 202.5) return 'جنوب (S)';
|
|
if (deg >= 202.5 && deg < 247.5) return 'جنوب غرب (SW)';
|
|
if (deg >= 247.5 && deg < 292.5) return 'غرب (W)';
|
|
return 'شمال غرب (NW)';
|
|
}
|
|
|
|
/// Convert LatLng to standard MGRS String representation (Zone + Band + 100k Square ID + Easting/Northing)
|
|
static String latLngToMgrs(double lat, double lng, {int accuracyDigits = 4}) {
|
|
if (lat < -80.0 || lat > 84.0) {
|
|
return 'OUT_OF_BOUNDS';
|
|
}
|
|
|
|
final zone = ((lng + 180.0) / 6.0).floor() + 1;
|
|
final coords = fromLatLng(LatLng(lat, lng), zone: zone);
|
|
|
|
final bandLetter = _getLatitudeBandLetter(lat);
|
|
final squareId = _get100kSquareId(coords.easting, coords.northing, zone);
|
|
|
|
final eInt = coords.easting.round() % 100000;
|
|
final nInt = coords.northing.round() % 100000;
|
|
|
|
// Pad to 5 digits then trim to desired accuracy
|
|
final e5 = eInt.toString().padLeft(5, '0');
|
|
final n5 = nInt.toString().padLeft(5, '0');
|
|
|
|
final eStr = e5.substring(0, math.min(5, accuracyDigits));
|
|
final nStr = n5.substring(0, math.min(5, accuracyDigits));
|
|
|
|
return '$zone$bandLetter $squareId $eStr $nStr';
|
|
}
|
|
|
|
/// Latitude band letter (8-degree bands from -80° to +84°: C to X, skipping I and O)
|
|
static String _getLatitudeBandLetter(double lat) {
|
|
const bands = 'CDEFGHJKLMNPQRSTUVWX';
|
|
if (lat >= 84.0) return 'X';
|
|
if (lat < -80.0) return 'C';
|
|
int index = ((lat + 80.0) / 8.0).floor();
|
|
if (index < 0) index = 0;
|
|
if (index >= bands.length) index = bands.length - 1;
|
|
return bands[index];
|
|
}
|
|
|
|
/// 100,000-meter Square Identification (2 letters) per US DMA TM 8358.1 / WGS84 standard
|
|
static String _get100kSquareId(double easting, double northing, int zone) {
|
|
const setColOrigin = 'AJSAJS';
|
|
const setRowOrigin = 'AFAFAF';
|
|
const aCode = 65; // 'A'
|
|
const iCode = 73; // 'I'
|
|
const oCode = 79; // 'O'
|
|
const vCode = 86; // 'V'
|
|
const zCode = 90; // 'Z'
|
|
|
|
final setParm = (zone % 6 == 0) ? 6 : (zone % 6);
|
|
final setIndex = setParm - 1;
|
|
|
|
final colOrigin = setColOrigin.codeUnitAt(setIndex);
|
|
final rowOrigin = setRowOrigin.codeUnitAt(setIndex);
|
|
|
|
final col = (easting / 100000.0).floor();
|
|
final row = (northing / 100000.0).floor() % 20;
|
|
|
|
var colInt = colOrigin + col - 1;
|
|
var rowInt = rowOrigin + row;
|
|
var colRollover = false;
|
|
|
|
if (colInt > zCode) {
|
|
colInt = colInt - zCode + aCode - 1;
|
|
colRollover = true;
|
|
}
|
|
if (colInt == iCode || (colOrigin < iCode && colInt > iCode) || ((colInt > iCode || colOrigin < iCode) && colRollover)) {
|
|
colInt++;
|
|
}
|
|
if (colInt == oCode || (colOrigin < oCode && colInt > oCode) || ((colInt > oCode || colOrigin < oCode) && colRollover)) {
|
|
colInt++;
|
|
if (colInt == iCode) colInt++;
|
|
}
|
|
if (colInt > zCode) {
|
|
colInt = colInt - zCode + aCode - 1;
|
|
}
|
|
|
|
var rowRollover = false;
|
|
if (rowInt > vCode) {
|
|
rowInt = rowInt - vCode + aCode - 1;
|
|
rowRollover = true;
|
|
}
|
|
if (rowInt == iCode || (rowOrigin < iCode && rowInt > iCode) || ((rowInt > iCode || rowOrigin < iCode) && rowRollover)) {
|
|
rowInt++;
|
|
}
|
|
if (rowInt == oCode || (rowOrigin < oCode && rowInt > oCode) || ((rowInt > oCode || rowOrigin < oCode) && rowRollover)) {
|
|
rowInt++;
|
|
if (rowInt == iCode) rowInt++;
|
|
}
|
|
if (rowInt > vCode) {
|
|
rowInt = rowInt - vCode + aCode - 1;
|
|
}
|
|
|
|
return String.fromCharCode(colInt) + String.fromCharCode(rowInt);
|
|
}
|
|
|
|
/// Convert WGS84 Lat/Lng to UTM Zone 36N Easting (شرقيات) and Northing (شماليات)
|
|
static MilitaryCoordinates fromLatLng(LatLng latLng, {int zone = 36}) {
|
|
final lat = latLng.latitude;
|
|
final lng = latLng.longitude;
|
|
|
|
final latRad = lat * (math.pi / 180.0);
|
|
final lngRad = lng * (math.pi / 180.0);
|
|
|
|
final centralMeridianDeg = (zone - 1) * 6 - 180 + 3;
|
|
final centralMeridianRad = centralMeridianDeg * (math.pi / 180.0);
|
|
|
|
final n = _a / math.sqrt(1.0 - _eSq * math.sin(latRad) * math.sin(latRad));
|
|
final t = math.tan(latRad) * math.tan(latRad);
|
|
final c = _ePrimeSq * math.cos(latRad) * math.cos(latRad);
|
|
final aCoeff = math.cos(latRad) * (lngRad - centralMeridianRad);
|
|
|
|
final m = _a *
|
|
((1.0 -
|
|
_eSq / 4.0 -
|
|
3.0 * _eSq * _eSq / 64.0 -
|
|
5.0 * _eSq * _eSq * _eSq / 256.0) *
|
|
latRad -
|
|
(3.0 * _eSq / 8.0 +
|
|
3.0 * _eSq * _eSq / 32.0 +
|
|
45.0 * _eSq * _eSq * _eSq / 1024.0) *
|
|
math.sin(2.0 * latRad) +
|
|
(15.0 * _eSq * _eSq / 256.0 +
|
|
45.0 * _eSq * _eSq * _eSq / 1024.0) *
|
|
math.sin(4.0 * latRad) -
|
|
(35.0 * _eSq * _eSq * _eSq / 3072.0) * math.sin(6.0 * latRad));
|
|
|
|
final easting = 500000.0 +
|
|
_k0 *
|
|
n *
|
|
(aCoeff +
|
|
(1.0 - t + c) * math.pow(aCoeff, 3) / 6.0 +
|
|
(5.0 - 18.0 * t + t * t + 72.0 * c - 58.0 * _ePrimeSq) *
|
|
math.pow(aCoeff, 5) /
|
|
120.0);
|
|
|
|
final northing = _k0 *
|
|
(m +
|
|
n *
|
|
math.tan(latRad) *
|
|
(aCoeff * aCoeff / 2.0 +
|
|
(5.0 - t + 9.0 * c + 4.0 * c * c) *
|
|
math.pow(aCoeff, 4) /
|
|
24.0 +
|
|
(61.0 - 58.0 * t + t * t + 600.0 * c - 330.0 * _ePrimeSq) *
|
|
math.pow(aCoeff, 6) /
|
|
720.0));
|
|
|
|
return MilitaryCoordinates(
|
|
lat: lat,
|
|
lng: lng,
|
|
easting: easting,
|
|
northing: northing,
|
|
zone: zone,
|
|
);
|
|
}
|
|
|
|
/// Convert UTM Zone 36N Easting (شرقيات) and Northing (شماليات) to WGS84 Lat/Lng
|
|
static LatLng toLatLng({
|
|
required double easting,
|
|
required double northing,
|
|
int zone = 36,
|
|
}) {
|
|
final e1 = (1.0 - math.sqrt(1.0 - _eSq)) / (1.0 + math.sqrt(1.0 - _eSq));
|
|
final x = easting - 500000.0;
|
|
final y = northing;
|
|
|
|
final m = y / _k0;
|
|
final mu = m /
|
|
(_a *
|
|
(1.0 -
|
|
_eSq / 4.0 -
|
|
3.0 * _eSq * _eSq / 64.0 -
|
|
5.0 * _eSq * _eSq * _eSq / 256.0));
|
|
|
|
final phi1Rad = mu +
|
|
(3.0 * e1 / 2.0 - 27.0 * math.pow(e1, 3) / 32.0) * math.sin(2.0 * mu) +
|
|
(21.0 * e1 * e1 / 16.0 - 55.0 * math.pow(e1, 4) / 32.0) *
|
|
math.sin(4.0 * mu) +
|
|
(151.0 * math.pow(e1, 3) / 96.0) * math.sin(6.0 * mu) +
|
|
(1097.0 * math.pow(e1, 4) / 512.0) * math.sin(8.0 * mu);
|
|
|
|
final n1 = _a /
|
|
math.sqrt(1.0 - _eSq * math.sin(phi1Rad) * math.sin(phi1Rad));
|
|
final t1 = math.tan(phi1Rad) * math.tan(phi1Rad);
|
|
final c1 = _ePrimeSq * math.cos(phi1Rad) * math.cos(phi1Rad);
|
|
final r1 = _a *
|
|
(1.0 - _eSq) /
|
|
math.pow(1.0 - _eSq * math.sin(phi1Rad) * math.sin(phi1Rad), 1.5);
|
|
final d = x / (n1 * _k0);
|
|
|
|
final latRad = phi1Rad -
|
|
(n1 * math.tan(phi1Rad) / r1) *
|
|
(d * d / 2.0 -
|
|
(5.0 + 3.0 * t1 + 10.0 * c1 - 4.0 * c1 * c1 - 9.0 * _ePrimeSq) *
|
|
math.pow(d, 4) /
|
|
24.0 +
|
|
(61.0 +
|
|
90.0 * t1 +
|
|
298.0 * c1 +
|
|
45.0 * t1 * t1 -
|
|
252.0 * _ePrimeSq -
|
|
3.0 * c1 * c1) *
|
|
math.pow(d, 6) /
|
|
720.0);
|
|
|
|
final centralMeridianDeg = (zone - 1) * 6 - 180 + 3;
|
|
final centralMeridianRad = centralMeridianDeg * (math.pi / 180.0);
|
|
|
|
final lngRad = centralMeridianRad +
|
|
(d -
|
|
(1.0 + 2.0 * t1 + c1) * math.pow(d, 3) / 6.0 +
|
|
(5.0 -
|
|
2.0 * c1 +
|
|
28.0 * t1 -
|
|
3.0 * c1 * c1 +
|
|
8.0 * _ePrimeSq +
|
|
24.0 * t1 * t1) *
|
|
math.pow(d, 5) /
|
|
120.0) /
|
|
math.cos(phi1Rad);
|
|
|
|
return LatLng(
|
|
latRad * (180.0 / math.pi),
|
|
lngRad * (180.0 / math.pi),
|
|
);
|
|
}
|
|
}
|
|
|
|
class MilitaryCoordinates {
|
|
final double lat;
|
|
final double lng;
|
|
final double easting;
|
|
final double northing;
|
|
final int zone;
|
|
|
|
MilitaryCoordinates({
|
|
required this.lat,
|
|
required this.lng,
|
|
required this.easting,
|
|
required this.northing,
|
|
this.zone = 36,
|
|
});
|
|
|
|
/// Short 6-digit Easting / Northing string for tactical voice transmission (e.g. 845 412)
|
|
String get shortGrid =>
|
|
'${(easting.round() % 100000 ~/ 100).toString().padLeft(3, '0')} ${(northing.round() % 100000 ~/ 100).toString().padLeft(3, '0')}';
|
|
|
|
/// Standard Arabic military format (شرقيات: 784520 م • شماليات: 3541280 م)
|
|
String get arabicFullFormat =>
|
|
'شرقيات: ${easting.round()} م • شماليات: ${northing.round()} م (${zone}N)';
|
|
|
|
String get eastingStr => '${easting.round()}';
|
|
String get northingStr => '${northing.round()}';
|
|
}
|