fix(tactical): sync complete tactical service with LOS and artillery methods
This commit is contained in:
@@ -1,41 +1,261 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { RedisService } from '../common/redis.service';
|
||||
import { ArtilleryMissionRequestDto, TacticalSymbolDto } from './dto/tactical.dto';
|
||||
|
||||
export interface ArtilleryMissionRequest {
|
||||
gunLat: number;
|
||||
gunLng: number;
|
||||
targetLat: number;
|
||||
targetLng: number;
|
||||
gunElevationOffset?: number; // meters above ground
|
||||
targetElevationOffset?: number; // meters above ground
|
||||
chargeType?: string; // Low, Med, High
|
||||
muzzleVelocity?: number; // m/s (default: 827 m/s for 155mm M109/M777)
|
||||
caliber?: string; // e.g. "155mm", "122mm", "120mm Mortar"
|
||||
}
|
||||
|
||||
export interface TacticalSymbol {
|
||||
id: string;
|
||||
type: 'friendly' | 'hostile' | 'neutral' | 'unknown' | 'radar' | 'artillery' | 'minefield' | 'checkpoint' | 'hlz' | 'op';
|
||||
name: string;
|
||||
name_ar: string;
|
||||
export interface LosPoint {
|
||||
index: number;
|
||||
distanceMeters: number;
|
||||
lat: number;
|
||||
lng: number;
|
||||
elevation?: number;
|
||||
notes?: string;
|
||||
timeAdded: string;
|
||||
groundElevationMeters: number;
|
||||
earthCurvatureSagittaMeters: number;
|
||||
apparentElevationMeters: number;
|
||||
sightRayElevationMeters: number;
|
||||
marginMeters: number;
|
||||
isVisible: boolean;
|
||||
isDeadGround: boolean;
|
||||
horizonAngleMils: number;
|
||||
}
|
||||
|
||||
export interface LineOfSightResponse {
|
||||
isDirectlyVisible: boolean;
|
||||
status: 'CLEAR_LINE_OF_SIGHT' | 'OBSTRUCTED';
|
||||
statusAr: string;
|
||||
summary: {
|
||||
totalDistanceMeters: number;
|
||||
totalDistanceKm: number;
|
||||
azimuthDegrees: number;
|
||||
verticalAngleDegrees: number;
|
||||
verticalAngleMilsNato: number;
|
||||
verticalAngleMilsSoviet: number;
|
||||
observerGroundElevationMeters: number;
|
||||
observerTotalElevationMeters: number;
|
||||
targetGroundElevationMeters: number;
|
||||
targetTotalElevationMeters: number;
|
||||
minElevationMeters: number;
|
||||
maxElevationMeters: number;
|
||||
deadGroundPercentage: number;
|
||||
samplePointsCount: number;
|
||||
stepMeters?: number;
|
||||
};
|
||||
highestObstacle: {
|
||||
distanceMeters: number;
|
||||
lat: number;
|
||||
lng: number;
|
||||
elevationMeters: number;
|
||||
sightRayElevationMeters: number;
|
||||
penetrationMeters: number;
|
||||
} | null;
|
||||
observer: {
|
||||
lat: number;
|
||||
lng: number;
|
||||
heightOffsetMeters: number;
|
||||
groundElevationMeters: number;
|
||||
totalElevationMeters: number;
|
||||
};
|
||||
target: {
|
||||
lat: number;
|
||||
lng: number;
|
||||
heightOffsetMeters: number;
|
||||
groundElevationMeters: number;
|
||||
totalElevationMeters: number;
|
||||
};
|
||||
profile?: LosPoint[];
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class TacticalService {
|
||||
private readonly logger = new Logger(TacticalService.name);
|
||||
|
||||
constructor(private readonly redisService: RedisService) { }
|
||||
constructor(private readonly redisService: RedisService) {}
|
||||
|
||||
/**
|
||||
* Tactical Line of Sight (LOS) & Intervisibility Engine
|
||||
*/
|
||||
async computeLineOfSight(
|
||||
observerLat: number,
|
||||
observerLng: number,
|
||||
targetLat: number,
|
||||
targetLng: number,
|
||||
observerHeight: number = 2,
|
||||
targetHeight: number = 2,
|
||||
samplesCount?: number,
|
||||
stepMeters?: number,
|
||||
compact: boolean = false,
|
||||
): Promise<LineOfSightResponse> {
|
||||
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: LosPoint[] = [];
|
||||
let isDirectlyVisible = true;
|
||||
let highestObstacle: LineOfSightResponse['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 }),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates ballistic artillery trajectory and checks terrain crest clearance
|
||||
* حساب مسار القذيفة البالستي القوسي وفحص أمان مرورها فوق القمم التضاريسية
|
||||
*/
|
||||
async calculateArtilleryFireMission(dto: ArtilleryMissionRequest) {
|
||||
async calculateArtilleryFireMission(dto: ArtilleryMissionRequestDto) {
|
||||
const {
|
||||
gunLat,
|
||||
gunLng,
|
||||
@@ -43,15 +263,13 @@ export class TacticalService {
|
||||
targetLng,
|
||||
gunElevationOffset = 2,
|
||||
targetElevationOffset = 0,
|
||||
muzzleVelocity = 827, // 155mm Howitzer standard muzzle velocity
|
||||
caliber = '155mm Howitzer'
|
||||
muzzleVelocity = 827,
|
||||
caliber = '155mm Howitzer',
|
||||
} = dto;
|
||||
|
||||
// Calculate distance and azimuth
|
||||
const distanceMeters = this.haversineDistance(gunLat, gunLng, targetLat, targetLng);
|
||||
const azimuthDegrees = this.calculateBearing(gunLat, gunLng, targetLat, targetLng);
|
||||
|
||||
// Approximate ground elevations for Levant / Jordan
|
||||
const gunGroundElev = this.estimateElevation(gunLat, gunLng);
|
||||
const targetGroundElev = this.estimateElevation(targetLat, targetLng);
|
||||
|
||||
@@ -59,42 +277,34 @@ export class TacticalService {
|
||||
const targetTotalElev = targetGroundElev + targetElevationOffset;
|
||||
const heightDiff = targetTotalElev - gunTotalElev;
|
||||
|
||||
// Ballistic calculation: Parabolic trajectory with air drag & gravity
|
||||
const g = 9.80665;
|
||||
const v0 = muzzleVelocity;
|
||||
|
||||
// Solve for firing angle theta: standard ballistic formula
|
||||
// v^4 - g * (g * x^2 + 2 * y * v^2)
|
||||
const term = Math.pow(v0, 4) - g * (g * Math.pow(distanceMeters, 2) + 2 * heightDiff * Math.pow(v0, 2));
|
||||
|
||||
let lowAngleRad = 0;
|
||||
let highAngleRad = 0;
|
||||
let maxRange = (Math.pow(v0, 2) / g);
|
||||
let isInRange = term >= 0 && distanceMeters <= maxRange;
|
||||
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 {
|
||||
// Default to 45 degrees if target is at extreme theoretical edge
|
||||
lowAngleRad = (45 * Math.PI) / 180;
|
||||
highAngleRad = (60 * Math.PI) / 180;
|
||||
}
|
||||
|
||||
const lowAngleDeg = (lowAngleRad * 180) / Math.PI;
|
||||
const lowAngleMils = (lowAngleDeg * (6400 / 360)); // NATO Mils (6400 mils in 360 deg)
|
||||
const lowAngleMils = lowAngleDeg * (6400 / 360);
|
||||
|
||||
const highAngleDeg = (highAngleRad * 180) / Math.PI;
|
||||
const highAngleMils = (highAngleDeg * (6400 / 360));
|
||||
const highAngleMils = highAngleDeg * (6400 / 360);
|
||||
|
||||
// Time of Flight (TOF) = x / (v0 * cos(theta))
|
||||
const timeOfFlightSeconds = distanceMeters / (v0 * Math.cos(lowAngleRad));
|
||||
const apexHeightMeters = gunTotalElev + Math.pow(v0 * Math.sin(lowAngleRad), 2) / (2 * g);
|
||||
|
||||
// Maximum Ordinate (Apex / أعلى نقطة في مسار القذيفة)
|
||||
const apexHeightMeters = gunTotalElev + (Math.pow(v0 * Math.sin(lowAngleRad), 2) / (2 * g));
|
||||
|
||||
// Generate 50 points along the trajectory arc and check for terrain collisions
|
||||
const trajectoryPoints: any[] = [];
|
||||
const samples = 50;
|
||||
let hasCrestClearance = true;
|
||||
@@ -106,9 +316,8 @@ export class TacticalService {
|
||||
const lat = gunLat + (targetLat - gunLat) * frac;
|
||||
const lng = gunLng + (targetLng - gunLng) * frac;
|
||||
|
||||
// Parabolic projectile altitude AMSL
|
||||
const t = frac * timeOfFlightSeconds;
|
||||
const y = (v0 * Math.sin(lowAngleRad) * t) - (0.5 * g * Math.pow(t, 2));
|
||||
const y = v0 * Math.sin(lowAngleRad) * t - 0.5 * g * Math.pow(t, 2);
|
||||
const projectileAlt = gunTotalElev + y;
|
||||
|
||||
const terrainElev = this.estimateElevation(lat, lng);
|
||||
@@ -123,7 +332,7 @@ export class TacticalService {
|
||||
projectileAltMeters: Math.round(projectileAlt),
|
||||
deficitMeters: Math.round(Math.abs(clearance)),
|
||||
lat,
|
||||
lng
|
||||
lng,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -134,7 +343,7 @@ export class TacticalService {
|
||||
lng,
|
||||
terrainElevation: Math.round(terrainElev),
|
||||
projectileAltitude: Math.round(projectileAlt),
|
||||
clearanceMeters: Math.round(clearance)
|
||||
clearanceMeters: Math.round(clearance),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -160,21 +369,20 @@ export class TacticalService {
|
||||
},
|
||||
hasCrestClearance,
|
||||
criticalObstacle,
|
||||
trajectoryPoints
|
||||
trajectoryPoints,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helicopter Landing Zone (HLZ) Assessment
|
||||
* كشف وتحليل صلاحية مهابط المروحيات والإخلاء الطبي (MEDEVAC)
|
||||
*/
|
||||
async assessHelicopterLandingZones(lat: number, lng: number, radiusMeters: number = 3000) {
|
||||
const candidates: any[] = [];
|
||||
const samples = 16; // Grid samples around center
|
||||
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 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);
|
||||
@@ -183,10 +391,13 @@ export class TacticalService {
|
||||
const hlzLng = lng + dLng;
|
||||
|
||||
const centerElev = this.estimateElevation(hlzLat, hlzLng);
|
||||
// Slope test around 50m radius
|
||||
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 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;
|
||||
|
||||
@@ -200,7 +411,7 @@ export class TacticalService {
|
||||
suitability: isSuitable ? 'EXCELLENT' : slopeDeg <= 12 ? 'MARGINAL' : 'UNSUITABLE',
|
||||
suitability_ar: isSuitable ? 'ممتاز (مستوي وخالي من العوائق)' : slopeDeg <= 12 ? 'مقبول بحذر' : 'غير صالح (شديد الانحدار)',
|
||||
maxRotorDiameterMeters: isSuitable ? 25 : 15,
|
||||
windApproachBearingDeg: Math.round(Math.random() * 360)
|
||||
windApproachBearingDeg: Math.round(Math.random() * 360),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -209,26 +420,26 @@ export class TacticalService {
|
||||
centerLng: lng,
|
||||
searchRadiusMeters: radiusMeters,
|
||||
totalAssessed: candidates.length,
|
||||
suitableCount: candidates.filter(c => c.suitability === 'EXCELLENT').length,
|
||||
zones: candidates
|
||||
suitableCount: candidates.filter((c) => c.suitability === 'EXCELLENT').length,
|
||||
zones: candidates,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Save tactical symbols scenario
|
||||
*/
|
||||
async saveScenario(name: string, symbols: TacticalSymbol[]) {
|
||||
async saveScenario(name: string, symbols: TacticalSymbolDto[]) {
|
||||
const key = `tactical:scenario:${name}`;
|
||||
await this.redisService.set(key, JSON.stringify(symbols), 86400 * 7); // 7 days
|
||||
await this.redisService.set(key, JSON.stringify(symbols), 86400 * 7);
|
||||
return { success: true, count: symbols.length, name };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get saved tactical symbols scenario
|
||||
*/
|
||||
async getScenario(name: string): Promise<TacticalSymbol[]> {
|
||||
async getScenario(name: string): Promise<TacticalSymbolDto[]> {
|
||||
const key = `tactical:scenario:${name}`;
|
||||
const data = await this.redisService.get<TacticalSymbol[]>(key);
|
||||
const data = await this.redisService.get<TacticalSymbolDto[]>(key);
|
||||
return data || [];
|
||||
}
|
||||
|
||||
@@ -239,8 +450,10 @@ export class TacticalService {
|
||||
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);
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -249,8 +462,10 @@ export class TacticalService {
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
private estimateElevation(lat: number, lng: number): number {
|
||||
|
||||
Reference in New Issue
Block a user