353 lines
17 KiB
JavaScript
353 lines
17 KiB
JavaScript
"use strict";
|
|
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
};
|
|
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
};
|
|
var TacticalService_1;
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.TacticalService = void 0;
|
|
const common_1 = require("@nestjs/common");
|
|
const redis_service_1 = require("../common/redis.service");
|
|
let TacticalService = TacticalService_1 = class TacticalService {
|
|
redisService;
|
|
logger = new common_1.Logger(TacticalService_1.name);
|
|
constructor(redisService) {
|
|
this.redisService = redisService;
|
|
}
|
|
async computeLineOfSight(observerLat, observerLng, targetLat, targetLng, observerHeight = 2, targetHeight = 2, samplesCount, stepMeters, compact = false) {
|
|
const totalDistanceMeters = this.haversineDistance(observerLat, observerLng, targetLat, targetLng);
|
|
let effectiveStep = stepMeters;
|
|
let effectiveSamples = samplesCount;
|
|
if (effectiveStep != null && effectiveStep > 0) {
|
|
effectiveSamples = Math.max(5, Math.min(300, Math.round(totalDistanceMeters / effectiveStep) + 1));
|
|
}
|
|
else if (effectiveSamples != null && effectiveSamples >= 5) {
|
|
effectiveSamples = Math.min(300, effectiveSamples);
|
|
effectiveStep = totalDistanceMeters / (effectiveSamples - 1);
|
|
}
|
|
else {
|
|
if (totalDistanceMeters <= 500) {
|
|
effectiveStep = 5;
|
|
}
|
|
else if (totalDistanceMeters <= 2000) {
|
|
effectiveStep = 10;
|
|
}
|
|
else if (totalDistanceMeters <= 10000) {
|
|
effectiveStep = 25;
|
|
}
|
|
else if (totalDistanceMeters <= 30000) {
|
|
effectiveStep = 50;
|
|
}
|
|
else {
|
|
effectiveStep = 100;
|
|
}
|
|
effectiveSamples = Math.max(10, Math.min(200, Math.round(totalDistanceMeters / effectiveStep) + 1));
|
|
}
|
|
const azimuthDegrees = this.calculateBearing(observerLat, observerLng, targetLat, targetLng);
|
|
const observerGround = this.estimateElevation(observerLat, observerLng);
|
|
const targetGround = this.estimateElevation(targetLat, targetLng);
|
|
const observerTotal = observerGround + observerHeight;
|
|
const targetTotal = targetGround + targetHeight;
|
|
const R_earth = 6371000;
|
|
const k_refraction = 0.13;
|
|
const effectiveRadius = R_earth / (1 - k_refraction);
|
|
const points = [];
|
|
let isDirectlyVisible = true;
|
|
let highestObstacle = null;
|
|
let maxPenetration = 0;
|
|
let minElev = Math.min(observerGround, targetGround);
|
|
let maxElev = Math.max(observerGround, targetGround);
|
|
let maxHorizonAngle = -Infinity;
|
|
let deadGroundCount = 0;
|
|
for (let i = 0; i < effectiveSamples; i++) {
|
|
const fraction = effectiveSamples === 1 ? 0 : i / (effectiveSamples - 1);
|
|
const dMeters = totalDistanceMeters * fraction;
|
|
const pLat = observerLat + (targetLat - observerLat) * fraction;
|
|
const pLng = observerLng + (targetLng - observerLng) * fraction;
|
|
const gElev = this.estimateElevation(pLat, pLng);
|
|
minElev = Math.min(minElev, gElev);
|
|
maxElev = Math.max(maxElev, gElev);
|
|
const d1 = dMeters;
|
|
const d2 = totalDistanceMeters - dMeters;
|
|
const sagitta = (d1 * d2) / (2 * effectiveRadius);
|
|
const apparentElev = gElev + sagitta;
|
|
const rayElev = observerTotal + (targetTotal - observerTotal) * fraction;
|
|
const margin = rayElev - apparentElev;
|
|
const isPointVisible = i === 0 || i === effectiveSamples - 1 || margin >= 0;
|
|
if (!isPointVisible) {
|
|
isDirectlyVisible = false;
|
|
const penetration = apparentElev - rayElev;
|
|
if (penetration > maxPenetration) {
|
|
maxPenetration = penetration;
|
|
highestObstacle = {
|
|
distanceMeters: Math.round(dMeters),
|
|
lat: Number(pLat.toFixed(6)),
|
|
lng: Number(pLng.toFixed(6)),
|
|
elevationMeters: Math.round(gElev),
|
|
sightRayElevationMeters: Math.round(rayElev),
|
|
penetrationMeters: Math.round(penetration * 10) / 10,
|
|
};
|
|
}
|
|
}
|
|
const angleFromObserver = dMeters > 0 ? (apparentElev - observerTotal) / dMeters : 0;
|
|
const angleMils = angleFromObserver * (6400 / (2 * Math.PI));
|
|
let isDeadGround = false;
|
|
if (i > 0) {
|
|
if (angleFromObserver < maxHorizonAngle) {
|
|
isDeadGround = true;
|
|
deadGroundCount++;
|
|
}
|
|
else {
|
|
maxHorizonAngle = angleFromObserver;
|
|
}
|
|
}
|
|
points.push({
|
|
index: i,
|
|
distanceMeters: Math.round(dMeters),
|
|
lat: Number(pLat.toFixed(6)),
|
|
lng: Number(pLng.toFixed(6)),
|
|
groundElevationMeters: Math.round(gElev),
|
|
earthCurvatureSagittaMeters: Math.round(sagitta * 10) / 10,
|
|
apparentElevationMeters: Math.round(apparentElev * 10) / 10,
|
|
sightRayElevationMeters: Math.round(rayElev * 10) / 10,
|
|
marginMeters: Math.round(margin * 10) / 10,
|
|
isVisible: isPointVisible,
|
|
isDeadGround,
|
|
horizonAngleMils: Math.round(angleMils * 10) / 10,
|
|
});
|
|
}
|
|
const elevDiff = targetTotal - observerTotal;
|
|
const verticalAngleRad = totalDistanceMeters > 0 ? Math.atan2(elevDiff, totalDistanceMeters) : 0;
|
|
const verticalAngleDegrees = (verticalAngleRad * 180) / Math.PI;
|
|
const verticalAngleMilsNato = (verticalAngleRad * 6400) / (2 * Math.PI);
|
|
const verticalAngleMilsSoviet = (verticalAngleRad * 6000) / (2 * Math.PI);
|
|
const deadGroundPercentage = effectiveSamples > 1
|
|
? Math.round((deadGroundCount / (effectiveSamples - 1)) * 100)
|
|
: 0;
|
|
return {
|
|
isDirectlyVisible,
|
|
status: isDirectlyVisible ? 'CLEAR_LINE_OF_SIGHT' : 'OBSTRUCTED',
|
|
statusAr: isDirectlyVisible ? 'رؤية مباشرة مكشوفة (Clear LOS)' : 'خط الرؤية محجوب بتضاريس عائقة (Obstructed)',
|
|
summary: {
|
|
totalDistanceMeters: Math.round(totalDistanceMeters),
|
|
totalDistanceKm: Math.round((totalDistanceMeters / 1000) * 100) / 100,
|
|
azimuthDegrees: Math.round(azimuthDegrees * 10) / 10,
|
|
verticalAngleDegrees: Math.round(verticalAngleDegrees * 100) / 100,
|
|
verticalAngleMilsNato: Math.round(verticalAngleMilsNato * 10) / 10,
|
|
verticalAngleMilsSoviet: Math.round(verticalAngleMilsSoviet * 10) / 10,
|
|
observerGroundElevationMeters: Math.round(observerGround),
|
|
observerTotalElevationMeters: Math.round(observerTotal),
|
|
targetGroundElevationMeters: Math.round(targetGround),
|
|
targetTotalElevationMeters: Math.round(targetTotal),
|
|
minElevationMeters: Math.round(minElev),
|
|
maxElevationMeters: Math.round(maxElev),
|
|
deadGroundPercentage,
|
|
samplePointsCount: effectiveSamples,
|
|
stepMeters: effectiveStep ? Math.round(effectiveStep * 10) / 10 : undefined,
|
|
},
|
|
highestObstacle,
|
|
observer: {
|
|
lat: observerLat,
|
|
lng: observerLng,
|
|
heightOffsetMeters: observerHeight,
|
|
groundElevationMeters: Math.round(observerGround),
|
|
totalElevationMeters: Math.round(observerTotal),
|
|
},
|
|
target: {
|
|
lat: targetLat,
|
|
lng: targetLng,
|
|
heightOffsetMeters: targetHeight,
|
|
groundElevationMeters: Math.round(targetGround),
|
|
totalElevationMeters: Math.round(targetTotal),
|
|
},
|
|
...(compact ? {} : { profile: points }),
|
|
};
|
|
}
|
|
async calculateArtilleryFireMission(dto) {
|
|
const { gunLat, gunLng, targetLat, targetLng, gunElevationOffset = 2, targetElevationOffset = 0, muzzleVelocity = 827, caliber = '155mm Howitzer', } = dto;
|
|
const distanceMeters = this.haversineDistance(gunLat, gunLng, targetLat, targetLng);
|
|
const azimuthDegrees = this.calculateBearing(gunLat, gunLng, targetLat, targetLng);
|
|
const gunGroundElev = this.estimateElevation(gunLat, gunLng);
|
|
const targetGroundElev = this.estimateElevation(targetLat, targetLng);
|
|
const gunTotalElev = gunGroundElev + gunElevationOffset;
|
|
const targetTotalElev = targetGroundElev + targetElevationOffset;
|
|
const heightDiff = targetTotalElev - gunTotalElev;
|
|
const g = 9.80665;
|
|
const v0 = muzzleVelocity;
|
|
const term = Math.pow(v0, 4) - g * (g * Math.pow(distanceMeters, 2) + 2 * heightDiff * Math.pow(v0, 2));
|
|
let lowAngleRad = 0;
|
|
let highAngleRad = 0;
|
|
const maxRange = Math.pow(v0, 2) / g;
|
|
const isInRange = term >= 0 && distanceMeters <= maxRange;
|
|
if (isInRange) {
|
|
const sqrtTerm = Math.sqrt(term);
|
|
lowAngleRad = Math.atan((Math.pow(v0, 2) - sqrtTerm) / (g * distanceMeters));
|
|
highAngleRad = Math.atan((Math.pow(v0, 2) + sqrtTerm) / (g * distanceMeters));
|
|
}
|
|
else {
|
|
lowAngleRad = (45 * Math.PI) / 180;
|
|
highAngleRad = (60 * Math.PI) / 180;
|
|
}
|
|
const lowAngleDeg = (lowAngleRad * 180) / Math.PI;
|
|
const lowAngleMils = lowAngleDeg * (6400 / 360);
|
|
const highAngleDeg = (highAngleRad * 180) / Math.PI;
|
|
const highAngleMils = highAngleDeg * (6400 / 360);
|
|
const timeOfFlightSeconds = distanceMeters / (v0 * Math.cos(lowAngleRad));
|
|
const apexHeightMeters = gunTotalElev + Math.pow(v0 * Math.sin(lowAngleRad), 2) / (2 * g);
|
|
const trajectoryPoints = [];
|
|
const samples = 50;
|
|
let hasCrestClearance = true;
|
|
let criticalObstacle = null;
|
|
for (let i = 0; i <= samples; i++) {
|
|
const frac = i / samples;
|
|
const d = distanceMeters * frac;
|
|
const lat = gunLat + (targetLat - gunLat) * frac;
|
|
const lng = gunLng + (targetLng - gunLng) * frac;
|
|
const t = frac * timeOfFlightSeconds;
|
|
const y = v0 * Math.sin(lowAngleRad) * t - 0.5 * g * Math.pow(t, 2);
|
|
const projectileAlt = gunTotalElev + y;
|
|
const terrainElev = this.estimateElevation(lat, lng);
|
|
const clearance = projectileAlt - terrainElev;
|
|
if (clearance <= 0 && i > 1 && i < samples) {
|
|
hasCrestClearance = false;
|
|
if (!criticalObstacle || clearance < criticalObstacle.clearance) {
|
|
criticalObstacle = {
|
|
distanceMeters: Math.round(d),
|
|
terrainElevMeters: Math.round(terrainElev),
|
|
projectileAltMeters: Math.round(projectileAlt),
|
|
deficitMeters: Math.round(Math.abs(clearance)),
|
|
lat,
|
|
lng,
|
|
};
|
|
}
|
|
}
|
|
trajectoryPoints.push({
|
|
distanceMeters: Math.round(d),
|
|
lat,
|
|
lng,
|
|
terrainElevation: Math.round(terrainElev),
|
|
projectileAltitude: Math.round(projectileAlt),
|
|
clearanceMeters: Math.round(clearance),
|
|
});
|
|
}
|
|
return {
|
|
fireMissionId: `FM-${Date.now().toString().slice(-6)}`,
|
|
caliber,
|
|
muzzleVelocityMs: muzzleVelocity,
|
|
distanceMeters: Math.round(distanceMeters),
|
|
distanceKm: Math.round((distanceMeters / 1000) * 100) / 100,
|
|
azimuthDegrees: Math.round(azimuthDegrees * 10) / 10,
|
|
azimuthMils: Math.round((azimuthDegrees * (6400 / 360)) * 10) / 10,
|
|
gunElevationMeters: Math.round(gunTotalElev),
|
|
targetElevationMeters: Math.round(targetTotalElev),
|
|
apexAltitudeMeters: Math.round(apexHeightMeters),
|
|
timeOfFlightSeconds: Math.round(timeOfFlightSeconds * 10) / 10,
|
|
lowAngle: {
|
|
degrees: Math.round(lowAngleDeg * 100) / 100,
|
|
mils: Math.round(lowAngleMils * 10) / 10,
|
|
},
|
|
highAngle: {
|
|
degrees: Math.round(highAngleDeg * 100) / 100,
|
|
mils: Math.round(highAngleMils * 10) / 10,
|
|
},
|
|
hasCrestClearance,
|
|
criticalObstacle,
|
|
trajectoryPoints,
|
|
};
|
|
}
|
|
async assessHelicopterLandingZones(lat, lng, radiusMeters = 3000) {
|
|
const candidates = [];
|
|
const samples = 16;
|
|
for (let i = 0; i < samples; i++) {
|
|
const angle = (i * 2 * Math.PI) / samples;
|
|
const r = radiusMeters * 0.3 + Math.random() * (radiusMeters * 0.6);
|
|
const dLat = (r / 6371000) * (180 / Math.PI) * Math.cos(angle);
|
|
const dLng = (r / (6371000 * Math.cos((lat * Math.PI) / 180))) * (180 / Math.PI) * Math.sin(angle);
|
|
const hlzLat = lat + dLat;
|
|
const hlzLng = lng + dLng;
|
|
const centerElev = this.estimateElevation(hlzLat, hlzLng);
|
|
const northElev = this.estimateElevation(hlzLat + 0.0005, hlzLng);
|
|
const eastElev = this.estimateElevation(hlzLat, hlzLng + 0.0005);
|
|
const slopeDeg = Math.round(Math.atan(Math.max(Math.abs(northElev - centerElev), Math.abs(eastElev - centerElev)) / 50) *
|
|
(180 / Math.PI) *
|
|
10) / 10;
|
|
const isSuitable = slopeDeg <= 7.0;
|
|
candidates.push({
|
|
id: `HLZ-${i + 1}`,
|
|
lat: hlzLat,
|
|
lng: hlzLng,
|
|
elevationMeters: Math.round(centerElev),
|
|
distanceMeters: Math.round(r),
|
|
slopeDegrees: slopeDeg,
|
|
suitability: isSuitable ? 'EXCELLENT' : slopeDeg <= 12 ? 'MARGINAL' : 'UNSUITABLE',
|
|
suitability_ar: isSuitable ? 'ممتاز (مستوي وخالي من العوائق)' : slopeDeg <= 12 ? 'مقبول بحذر' : 'غير صالح (شديد الانحدار)',
|
|
maxRotorDiameterMeters: isSuitable ? 25 : 15,
|
|
windApproachBearingDeg: Math.round(Math.random() * 360),
|
|
});
|
|
}
|
|
return {
|
|
centerLat: lat,
|
|
centerLng: lng,
|
|
searchRadiusMeters: radiusMeters,
|
|
totalAssessed: candidates.length,
|
|
suitableCount: candidates.filter((c) => c.suitability === 'EXCELLENT').length,
|
|
zones: candidates,
|
|
};
|
|
}
|
|
async saveScenario(name, symbols) {
|
|
const key = `tactical:scenario:${name}`;
|
|
await this.redisService.set(key, JSON.stringify(symbols), 86400 * 7);
|
|
return { success: true, count: symbols.length, name };
|
|
}
|
|
async getScenario(name) {
|
|
const key = `tactical:scenario:${name}`;
|
|
const data = await this.redisService.get(key);
|
|
return data || [];
|
|
}
|
|
haversineDistance(lat1, lon1, lat2, lon2) {
|
|
const R = 6371000;
|
|
const dLat = (lat2 - lat1) * (Math.PI / 180);
|
|
const dLon = (lon2 - lon1) * (Math.PI / 180);
|
|
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
|
Math.cos(lat1 * (Math.PI / 180)) *
|
|
Math.cos(lat2 * (Math.PI / 180)) *
|
|
Math.sin(dLon / 2) *
|
|
Math.sin(dLon / 2);
|
|
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
}
|
|
calculateBearing(lat1, lon1, lat2, lon2) {
|
|
const phi1 = (lat1 * Math.PI) / 180;
|
|
const phi2 = (lat2 * Math.PI) / 180;
|
|
const deltaLambda = ((lon2 - lon1) * Math.PI) / 180;
|
|
const y = Math.sin(deltaLambda) * Math.cos(phi2);
|
|
const x = Math.cos(phi1) * Math.sin(phi2) -
|
|
Math.sin(phi1) * Math.cos(phi2) * Math.cos(deltaLambda);
|
|
return ((Math.atan2(y, x) * 180) / Math.PI + 360) % 360;
|
|
}
|
|
estimateElevation(lat, lng) {
|
|
if (lng < 35.6 && lat < 32.2 && lat > 31.0) {
|
|
return -400 + Math.abs(lng - 35.5) * 3000;
|
|
}
|
|
if (lat >= 32.1 && lng < 36.0) {
|
|
return 850 + Math.sin(lat * 50) * 250 + Math.cos(lng * 40) * 150;
|
|
}
|
|
if (lat >= 31.8 && lat < 32.1 && lng >= 35.8 && lng < 36.2) {
|
|
return 900 + Math.sin((lat - 31.95) * 100) * 120 + Math.cos((lng - 35.9) * 100) * 100;
|
|
}
|
|
if (lat < 31.5 && lat > 30.0 && lng < 35.7) {
|
|
return 1100 + Math.sin(lat * 30) * 350;
|
|
}
|
|
return 650 + (lng - 36.0) * 30;
|
|
}
|
|
};
|
|
exports.TacticalService = TacticalService;
|
|
exports.TacticalService = TacticalService = TacticalService_1 = __decorate([
|
|
(0, common_1.Injectable)(),
|
|
__metadata("design:paramtypes", [redis_service_1.RedisService])
|
|
], TacticalService);
|
|
//# sourceMappingURL=tactical.service.js.map
|