feat(routing): enable elevation profile and steep slope/incline warnings in routing API and frontend
This commit is contained in:
+1
@@ -19,6 +19,7 @@ export declare class MapsController {
|
|||||||
points: any;
|
points: any;
|
||||||
bbox: any;
|
bbox: any;
|
||||||
instructions: any;
|
instructions: any;
|
||||||
|
elevationSummary: any;
|
||||||
alternatives: any;
|
alternatives: any;
|
||||||
}>;
|
}>;
|
||||||
getConfig(): Promise<{
|
getConfig(): Promise<{
|
||||||
|
|||||||
Vendored
+4
@@ -28,8 +28,12 @@ export declare class MapsService {
|
|||||||
points: any;
|
points: any;
|
||||||
bbox: any;
|
bbox: any;
|
||||||
instructions: any;
|
instructions: any;
|
||||||
|
elevationSummary: any;
|
||||||
alternatives: any;
|
alternatives: any;
|
||||||
}>;
|
}>;
|
||||||
|
private enrichInstructionsWithSlopeAnalysis;
|
||||||
|
private estimateElevation;
|
||||||
|
private haversineDistance;
|
||||||
private getRouteName;
|
private getRouteName;
|
||||||
private decodePolyline;
|
private decodePolyline;
|
||||||
getMapConfig(): Promise<{
|
getMapConfig(): Promise<{
|
||||||
|
|||||||
Vendored
+91
-1
@@ -158,6 +158,7 @@ let MapsService = class MapsService {
|
|||||||
const pTrafficFactor = this.trafficGrid.getTrafficFactor(pCoords, hr, dow);
|
const pTrafficFactor = this.trafficGrid.getTrafficFactor(pCoords, hr, dow);
|
||||||
const pDuration = Math.round((p.time / 1000) * pTrafficFactor);
|
const pDuration = Math.round((p.time / 1000) * pTrafficFactor);
|
||||||
const routeName = this.getRouteName(p.instructions);
|
const routeName = this.getRouteName(p.instructions);
|
||||||
|
const { enrichedInstructions, slopeSummary } = this.enrichInstructionsWithSlopeAnalysis(p.instructions, pCoords);
|
||||||
const tags = [];
|
const tags = [];
|
||||||
if (index === 0)
|
if (index === 0)
|
||||||
tags.push('FASTEST');
|
tags.push('FASTEST');
|
||||||
@@ -175,7 +176,8 @@ let MapsService = class MapsService {
|
|||||||
duration: pDuration,
|
duration: pDuration,
|
||||||
points: p.points,
|
points: p.points,
|
||||||
bbox: p.bbox,
|
bbox: p.bbox,
|
||||||
instructions: steps ? p.instructions : undefined
|
instructions: steps ? enrichedInstructions : undefined,
|
||||||
|
elevationSummary: slopeSummary
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
const mainRoute = processedPaths[0];
|
const mainRoute = processedPaths[0];
|
||||||
@@ -191,6 +193,7 @@ let MapsService = class MapsService {
|
|||||||
points: mainRoute.points,
|
points: mainRoute.points,
|
||||||
bbox: mainRoute.bbox,
|
bbox: mainRoute.bbox,
|
||||||
instructions: mainRoute.instructions,
|
instructions: mainRoute.instructions,
|
||||||
|
elevationSummary: mainRoute.elevationSummary,
|
||||||
alternatives: altRoutes
|
alternatives: altRoutes
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -200,6 +203,93 @@ let MapsService = class MapsService {
|
|||||||
throw new common_1.HttpException(`Routing Failure: ${msg}`, common_1.HttpStatus.BAD_GATEWAY);
|
throw new common_1.HttpException(`Routing Failure: ${msg}`, common_1.HttpStatus.BAD_GATEWAY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
enrichInstructionsWithSlopeAnalysis(instructions, coords) {
|
||||||
|
if (!instructions || !coords || coords.length < 2) {
|
||||||
|
return {
|
||||||
|
enrichedInstructions: instructions,
|
||||||
|
slopeSummary: { totalAscentMeters: 0, totalDescentMeters: 0, maxInclinePercent: 0, maxDeclinePercent: 0, steepWarningsCount: 0, steepWarnings: [] }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let totalAscent = 0;
|
||||||
|
let totalDescent = 0;
|
||||||
|
let maxInclinePercent = 0;
|
||||||
|
let maxDeclinePercent = 0;
|
||||||
|
const steepWarnings = [];
|
||||||
|
const enrichedInstructions = instructions.map((inst) => {
|
||||||
|
const interval = inst.interval || [0, 0];
|
||||||
|
const startIdx = Math.min(interval[0], coords.length - 1);
|
||||||
|
const endIdx = Math.min(interval[1], coords.length - 1);
|
||||||
|
if (startIdx < endIdx) {
|
||||||
|
const startCoord = coords[startIdx];
|
||||||
|
const endCoord = coords[endIdx];
|
||||||
|
const startElev = this.estimateElevation(startCoord[1], startCoord[0]);
|
||||||
|
const endElev = this.estimateElevation(endCoord[1], endCoord[0]);
|
||||||
|
const elevDiff = endElev - startElev;
|
||||||
|
const dist = inst.distance || this.haversineDistance(startCoord[1], startCoord[0], endCoord[1], endCoord[0]) || 1;
|
||||||
|
if (elevDiff > 0)
|
||||||
|
totalAscent += elevDiff;
|
||||||
|
else
|
||||||
|
totalDescent += Math.abs(elevDiff);
|
||||||
|
const slopePercent = Math.round((elevDiff / Math.max(20, dist)) * 100);
|
||||||
|
if (slopePercent > maxInclinePercent)
|
||||||
|
maxInclinePercent = slopePercent;
|
||||||
|
if (slopePercent < maxDeclinePercent)
|
||||||
|
maxDeclinePercent = slopePercent;
|
||||||
|
let warning_ar = null;
|
||||||
|
if (slopePercent >= 9) {
|
||||||
|
warning_ar = `⚠️ تنبيه: صعود حاد (+${slopePercent}%)`;
|
||||||
|
steepWarnings.push({ text: inst.text, slopePercent, type: 'incline', street: inst.street_name });
|
||||||
|
}
|
||||||
|
else if (slopePercent <= -9) {
|
||||||
|
warning_ar = `⚠️ تنبيه: منحدر شديد (${slopePercent}%) - خفف السرعة`;
|
||||||
|
steepWarnings.push({ text: inst.text, slopePercent, type: 'decline', street: inst.street_name });
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...inst,
|
||||||
|
slopePercent,
|
||||||
|
elevationChangeMeters: Math.round(elevDiff),
|
||||||
|
slopeWarning: warning_ar || undefined,
|
||||||
|
text: warning_ar ? `${inst.text} (${warning_ar})` : inst.text
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return inst;
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
enrichedInstructions,
|
||||||
|
slopeSummary: {
|
||||||
|
totalAscentMeters: Math.round(totalAscent),
|
||||||
|
totalDescentMeters: Math.round(totalDescent),
|
||||||
|
maxInclinePercent: Math.round(maxInclinePercent),
|
||||||
|
maxDeclinePercent: Math.round(maxDeclinePercent),
|
||||||
|
steepWarningsCount: steepWarnings.length,
|
||||||
|
steepWarnings
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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));
|
||||||
|
}
|
||||||
getRouteName(instructions) {
|
getRouteName(instructions) {
|
||||||
if (!instructions || instructions.length === 0)
|
if (!instructions || instructions.length === 0)
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
+5
-5
@@ -4,10 +4,10 @@ import { TacticalService } from './tactical.service';
|
|||||||
export declare class TacticalController {
|
export declare class TacticalController {
|
||||||
private readonly tacticalService;
|
private readonly tacticalService;
|
||||||
constructor(tacticalService: TacticalService);
|
constructor(tacticalService: TacticalService);
|
||||||
getLineOfSight(query: LineOfSightQueryDto): Promise<any>;
|
getLineOfSight(query: LineOfSightQueryDto): Promise<import("./tactical.service").LineOfSightResponse>;
|
||||||
getLosAlias(query: LineOfSightQueryDto): Promise<any>;
|
getLosAlias(query: LineOfSightQueryDto): Promise<import("./tactical.service").LineOfSightResponse>;
|
||||||
postLineOfSight(body: LineOfSightBodyDto): Promise<any>;
|
postLineOfSight(body: LineOfSightBodyDto): Promise<import("./tactical.service").LineOfSightResponse>;
|
||||||
postLosAlias(body: LineOfSightBodyDto): Promise<any>;
|
postLosAlias(body: LineOfSightBodyDto): Promise<import("./tactical.service").LineOfSightResponse>;
|
||||||
calculateArtilleryFireMission(dto: ArtilleryMissionRequestDto): Promise<{
|
calculateArtilleryFireMission(dto: ArtilleryMissionRequestDto): Promise<{
|
||||||
fireMissionId: string;
|
fireMissionId: string;
|
||||||
caliber: string;
|
caliber: string;
|
||||||
@@ -45,5 +45,5 @@ export declare class TacticalController {
|
|||||||
count: number;
|
count: number;
|
||||||
name: string;
|
name: string;
|
||||||
}>;
|
}>;
|
||||||
getScenario(name?: string): Promise<import("./tactical.service").TacticalSymbol[]>;
|
getScenario(name?: string): Promise<import("./dto/tactical.dto").TacticalSymbolDto[]>;
|
||||||
}
|
}
|
||||||
|
|||||||
+61
-22
@@ -1,31 +1,70 @@
|
|||||||
import { RedisService } from '../common/redis.service';
|
import { RedisService } from '../common/redis.service';
|
||||||
export interface ArtilleryMissionRequest {
|
import { ArtilleryMissionRequestDto, TacticalSymbolDto } from './dto/tactical.dto';
|
||||||
gunLat: number;
|
export interface LosPoint {
|
||||||
gunLng: number;
|
index: number;
|
||||||
targetLat: number;
|
distanceMeters: number;
|
||||||
targetLng: number;
|
|
||||||
gunElevationOffset?: number;
|
|
||||||
targetElevationOffset?: number;
|
|
||||||
chargeType?: string;
|
|
||||||
muzzleVelocity?: number;
|
|
||||||
caliber?: string;
|
|
||||||
}
|
|
||||||
export interface TacticalSymbol {
|
|
||||||
id: string;
|
|
||||||
type: 'friendly' | 'hostile' | 'neutral' | 'unknown' | 'radar' | 'artillery' | 'minefield' | 'checkpoint' | 'hlz' | 'op';
|
|
||||||
name: string;
|
|
||||||
name_ar: string;
|
|
||||||
lat: number;
|
lat: number;
|
||||||
lng: number;
|
lng: number;
|
||||||
elevation?: number;
|
groundElevationMeters: number;
|
||||||
notes?: string;
|
earthCurvatureSagittaMeters: number;
|
||||||
timeAdded: string;
|
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[];
|
||||||
}
|
}
|
||||||
export declare class TacticalService {
|
export declare class TacticalService {
|
||||||
private readonly redisService;
|
private readonly redisService;
|
||||||
private readonly logger;
|
private readonly logger;
|
||||||
constructor(redisService: RedisService);
|
constructor(redisService: RedisService);
|
||||||
calculateArtilleryFireMission(dto: ArtilleryMissionRequest): Promise<{
|
computeLineOfSight(observerLat: number, observerLng: number, targetLat: number, targetLng: number, observerHeight?: number, targetHeight?: number, samplesCount?: number, stepMeters?: number, compact?: boolean): Promise<LineOfSightResponse>;
|
||||||
|
calculateArtilleryFireMission(dto: ArtilleryMissionRequestDto): Promise<{
|
||||||
fireMissionId: string;
|
fireMissionId: string;
|
||||||
caliber: string;
|
caliber: string;
|
||||||
muzzleVelocityMs: number;
|
muzzleVelocityMs: number;
|
||||||
@@ -57,12 +96,12 @@ export declare class TacticalService {
|
|||||||
suitableCount: number;
|
suitableCount: number;
|
||||||
zones: any[];
|
zones: any[];
|
||||||
}>;
|
}>;
|
||||||
saveScenario(name: string, symbols: TacticalSymbol[]): Promise<{
|
saveScenario(name: string, symbols: TacticalSymbolDto[]): Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
count: number;
|
count: number;
|
||||||
name: string;
|
name: string;
|
||||||
}>;
|
}>;
|
||||||
getScenario(name: string): Promise<TacticalSymbol[]>;
|
getScenario(name: string): Promise<TacticalSymbolDto[]>;
|
||||||
private haversineDistance;
|
private haversineDistance;
|
||||||
private calculateBearing;
|
private calculateBearing;
|
||||||
private estimateElevation;
|
private estimateElevation;
|
||||||
|
|||||||
+173
-19
@@ -19,8 +19,157 @@ let TacticalService = TacticalService_1 = class TacticalService {
|
|||||||
constructor(redisService) {
|
constructor(redisService) {
|
||||||
this.redisService = 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) {
|
async calculateArtilleryFireMission(dto) {
|
||||||
const { gunLat, gunLng, targetLat, targetLng, gunElevationOffset = 2, targetElevationOffset = 0, muzzleVelocity = 827, caliber = '155mm Howitzer' } = 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 distanceMeters = this.haversineDistance(gunLat, gunLng, targetLat, targetLng);
|
||||||
const azimuthDegrees = this.calculateBearing(gunLat, gunLng, targetLat, targetLng);
|
const azimuthDegrees = this.calculateBearing(gunLat, gunLng, targetLat, targetLng);
|
||||||
const gunGroundElev = this.estimateElevation(gunLat, gunLng);
|
const gunGroundElev = this.estimateElevation(gunLat, gunLng);
|
||||||
@@ -33,8 +182,8 @@ let TacticalService = TacticalService_1 = class TacticalService {
|
|||||||
const term = Math.pow(v0, 4) - g * (g * Math.pow(distanceMeters, 2) + 2 * heightDiff * Math.pow(v0, 2));
|
const term = Math.pow(v0, 4) - g * (g * Math.pow(distanceMeters, 2) + 2 * heightDiff * Math.pow(v0, 2));
|
||||||
let lowAngleRad = 0;
|
let lowAngleRad = 0;
|
||||||
let highAngleRad = 0;
|
let highAngleRad = 0;
|
||||||
let maxRange = (Math.pow(v0, 2) / g);
|
const maxRange = Math.pow(v0, 2) / g;
|
||||||
let isInRange = term >= 0 && distanceMeters <= maxRange;
|
const isInRange = term >= 0 && distanceMeters <= maxRange;
|
||||||
if (isInRange) {
|
if (isInRange) {
|
||||||
const sqrtTerm = Math.sqrt(term);
|
const sqrtTerm = Math.sqrt(term);
|
||||||
lowAngleRad = Math.atan((Math.pow(v0, 2) - sqrtTerm) / (g * distanceMeters));
|
lowAngleRad = Math.atan((Math.pow(v0, 2) - sqrtTerm) / (g * distanceMeters));
|
||||||
@@ -45,11 +194,11 @@ let TacticalService = TacticalService_1 = class TacticalService {
|
|||||||
highAngleRad = (60 * Math.PI) / 180;
|
highAngleRad = (60 * Math.PI) / 180;
|
||||||
}
|
}
|
||||||
const lowAngleDeg = (lowAngleRad * 180) / Math.PI;
|
const lowAngleDeg = (lowAngleRad * 180) / Math.PI;
|
||||||
const lowAngleMils = (lowAngleDeg * (6400 / 360));
|
const lowAngleMils = lowAngleDeg * (6400 / 360);
|
||||||
const highAngleDeg = (highAngleRad * 180) / Math.PI;
|
const highAngleDeg = (highAngleRad * 180) / Math.PI;
|
||||||
const highAngleMils = (highAngleDeg * (6400 / 360));
|
const highAngleMils = highAngleDeg * (6400 / 360);
|
||||||
const timeOfFlightSeconds = distanceMeters / (v0 * Math.cos(lowAngleRad));
|
const timeOfFlightSeconds = distanceMeters / (v0 * Math.cos(lowAngleRad));
|
||||||
const apexHeightMeters = gunTotalElev + (Math.pow(v0 * Math.sin(lowAngleRad), 2) / (2 * g));
|
const apexHeightMeters = gunTotalElev + Math.pow(v0 * Math.sin(lowAngleRad), 2) / (2 * g);
|
||||||
const trajectoryPoints = [];
|
const trajectoryPoints = [];
|
||||||
const samples = 50;
|
const samples = 50;
|
||||||
let hasCrestClearance = true;
|
let hasCrestClearance = true;
|
||||||
@@ -60,7 +209,7 @@ let TacticalService = TacticalService_1 = class TacticalService {
|
|||||||
const lat = gunLat + (targetLat - gunLat) * frac;
|
const lat = gunLat + (targetLat - gunLat) * frac;
|
||||||
const lng = gunLng + (targetLng - gunLng) * frac;
|
const lng = gunLng + (targetLng - gunLng) * frac;
|
||||||
const t = frac * timeOfFlightSeconds;
|
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 projectileAlt = gunTotalElev + y;
|
||||||
const terrainElev = this.estimateElevation(lat, lng);
|
const terrainElev = this.estimateElevation(lat, lng);
|
||||||
const clearance = projectileAlt - terrainElev;
|
const clearance = projectileAlt - terrainElev;
|
||||||
@@ -73,7 +222,7 @@ let TacticalService = TacticalService_1 = class TacticalService {
|
|||||||
projectileAltMeters: Math.round(projectileAlt),
|
projectileAltMeters: Math.round(projectileAlt),
|
||||||
deficitMeters: Math.round(Math.abs(clearance)),
|
deficitMeters: Math.round(Math.abs(clearance)),
|
||||||
lat,
|
lat,
|
||||||
lng
|
lng,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -83,7 +232,7 @@ let TacticalService = TacticalService_1 = class TacticalService {
|
|||||||
lng,
|
lng,
|
||||||
terrainElevation: Math.round(terrainElev),
|
terrainElevation: Math.round(terrainElev),
|
||||||
projectileAltitude: Math.round(projectileAlt),
|
projectileAltitude: Math.round(projectileAlt),
|
||||||
clearanceMeters: Math.round(clearance)
|
clearanceMeters: Math.round(clearance),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@@ -108,7 +257,7 @@ let TacticalService = TacticalService_1 = class TacticalService {
|
|||||||
},
|
},
|
||||||
hasCrestClearance,
|
hasCrestClearance,
|
||||||
criticalObstacle,
|
criticalObstacle,
|
||||||
trajectoryPoints
|
trajectoryPoints,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
async assessHelicopterLandingZones(lat, lng, radiusMeters = 3000) {
|
async assessHelicopterLandingZones(lat, lng, radiusMeters = 3000) {
|
||||||
@@ -116,7 +265,7 @@ let TacticalService = TacticalService_1 = class TacticalService {
|
|||||||
const samples = 16;
|
const samples = 16;
|
||||||
for (let i = 0; i < samples; i++) {
|
for (let i = 0; i < samples; i++) {
|
||||||
const angle = (i * 2 * Math.PI) / samples;
|
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 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 dLng = (r / (6371000 * Math.cos((lat * Math.PI) / 180))) * (180 / Math.PI) * Math.sin(angle);
|
||||||
const hlzLat = lat + dLat;
|
const hlzLat = lat + dLat;
|
||||||
@@ -124,7 +273,9 @@ let TacticalService = TacticalService_1 = class TacticalService {
|
|||||||
const centerElev = this.estimateElevation(hlzLat, hlzLng);
|
const centerElev = this.estimateElevation(hlzLat, hlzLng);
|
||||||
const northElev = this.estimateElevation(hlzLat + 0.0005, hlzLng);
|
const northElev = this.estimateElevation(hlzLat + 0.0005, hlzLng);
|
||||||
const eastElev = this.estimateElevation(hlzLat, hlzLng + 0.0005);
|
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;
|
const isSuitable = slopeDeg <= 7.0;
|
||||||
candidates.push({
|
candidates.push({
|
||||||
id: `HLZ-${i + 1}`,
|
id: `HLZ-${i + 1}`,
|
||||||
@@ -136,7 +287,7 @@ let TacticalService = TacticalService_1 = class TacticalService {
|
|||||||
suitability: isSuitable ? 'EXCELLENT' : slopeDeg <= 12 ? 'MARGINAL' : 'UNSUITABLE',
|
suitability: isSuitable ? 'EXCELLENT' : slopeDeg <= 12 ? 'MARGINAL' : 'UNSUITABLE',
|
||||||
suitability_ar: isSuitable ? 'ممتاز (مستوي وخالي من العوائق)' : slopeDeg <= 12 ? 'مقبول بحذر' : 'غير صالح (شديد الانحدار)',
|
suitability_ar: isSuitable ? 'ممتاز (مستوي وخالي من العوائق)' : slopeDeg <= 12 ? 'مقبول بحذر' : 'غير صالح (شديد الانحدار)',
|
||||||
maxRotorDiameterMeters: isSuitable ? 25 : 15,
|
maxRotorDiameterMeters: isSuitable ? 25 : 15,
|
||||||
windApproachBearingDeg: Math.round(Math.random() * 360)
|
windApproachBearingDeg: Math.round(Math.random() * 360),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@@ -144,8 +295,8 @@ let TacticalService = TacticalService_1 = class TacticalService {
|
|||||||
centerLng: lng,
|
centerLng: lng,
|
||||||
searchRadiusMeters: radiusMeters,
|
searchRadiusMeters: radiusMeters,
|
||||||
totalAssessed: candidates.length,
|
totalAssessed: candidates.length,
|
||||||
suitableCount: candidates.filter(c => c.suitability === 'EXCELLENT').length,
|
suitableCount: candidates.filter((c) => c.suitability === 'EXCELLENT').length,
|
||||||
zones: candidates
|
zones: candidates,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
async saveScenario(name, symbols) {
|
async saveScenario(name, symbols) {
|
||||||
@@ -163,8 +314,10 @@ let TacticalService = TacticalService_1 = class TacticalService {
|
|||||||
const dLat = (lat2 - lat1) * (Math.PI / 180);
|
const dLat = (lat2 - lat1) * (Math.PI / 180);
|
||||||
const dLon = (lon2 - lon1) * (Math.PI / 180);
|
const dLon = (lon2 - lon1) * (Math.PI / 180);
|
||||||
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||||
Math.cos(lat1 * (Math.PI / 180)) * Math.cos(lat2 * (Math.PI / 180)) *
|
Math.cos(lat1 * (Math.PI / 180)) *
|
||||||
Math.sin(dLon / 2) * Math.sin(dLon / 2);
|
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));
|
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||||
}
|
}
|
||||||
calculateBearing(lat1, lon1, lat2, lon2) {
|
calculateBearing(lat1, lon1, lat2, lon2) {
|
||||||
@@ -172,8 +325,9 @@ let TacticalService = TacticalService_1 = class TacticalService {
|
|||||||
const phi2 = (lat2 * Math.PI) / 180;
|
const phi2 = (lat2 * Math.PI) / 180;
|
||||||
const deltaLambda = ((lon2 - lon1) * Math.PI) / 180;
|
const deltaLambda = ((lon2 - lon1) * Math.PI) / 180;
|
||||||
const y = Math.sin(deltaLambda) * Math.cos(phi2);
|
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);
|
const x = Math.cos(phi1) * Math.sin(phi2) -
|
||||||
return (Math.atan2(y, x) * (180 / Math.PI) + 360) % 360;
|
Math.sin(phi1) * Math.cos(phi2) * Math.cos(deltaLambda);
|
||||||
|
return ((Math.atan2(y, x) * 180) / Math.PI + 360) % 360;
|
||||||
}
|
}
|
||||||
estimateElevation(lat, lng) {
|
estimateElevation(lat, lng) {
|
||||||
if (lng < 35.6 && lat < 32.2 && lat > 31.0) {
|
if (lng < 35.6 && lat < 32.2 && lat > 31.0) {
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
@@ -164,13 +164,16 @@ export class MapsService {
|
|||||||
const baseDuration = route.time / 1000;
|
const baseDuration = route.time / 1000;
|
||||||
const trafficAwareDuration = baseDuration * trafficFactor;
|
const trafficAwareDuration = baseDuration * trafficFactor;
|
||||||
|
|
||||||
// Process all paths to add metadata (Names, Tags)
|
// Process all paths to add metadata (Names, Tags, Elevation & Slope Warnings)
|
||||||
const processedPaths = paths.map((p: any, index: number) => {
|
const processedPaths = paths.map((p: any, index: number) => {
|
||||||
const pCoords = this.decodePolyline(p.points);
|
const pCoords = this.decodePolyline(p.points);
|
||||||
const pTrafficFactor = this.trafficGrid.getTrafficFactor(pCoords, hr, dow);
|
const pTrafficFactor = this.trafficGrid.getTrafficFactor(pCoords, hr, dow);
|
||||||
const pDuration = Math.round((p.time / 1000) * pTrafficFactor);
|
const pDuration = Math.round((p.time / 1000) * pTrafficFactor);
|
||||||
const routeName = this.getRouteName(p.instructions);
|
const routeName = this.getRouteName(p.instructions);
|
||||||
|
|
||||||
|
// Analyze slopes and enrich instructions
|
||||||
|
const { enrichedInstructions, slopeSummary } = this.enrichInstructionsWithSlopeAnalysis(p.instructions, pCoords);
|
||||||
|
|
||||||
// Tags assignment
|
// Tags assignment
|
||||||
const tags: string[] = [];
|
const tags: string[] = [];
|
||||||
if (index === 0) tags.push('FASTEST');
|
if (index === 0) tags.push('FASTEST');
|
||||||
@@ -187,7 +190,8 @@ export class MapsService {
|
|||||||
duration: pDuration,
|
duration: pDuration,
|
||||||
points: p.points,
|
points: p.points,
|
||||||
bbox: p.bbox,
|
bbox: p.bbox,
|
||||||
instructions: steps ? p.instructions : undefined
|
instructions: steps ? enrichedInstructions : undefined,
|
||||||
|
elevationSummary: slopeSummary
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -205,6 +209,7 @@ export class MapsService {
|
|||||||
points: mainRoute.points,
|
points: mainRoute.points,
|
||||||
bbox: mainRoute.bbox,
|
bbox: mainRoute.bbox,
|
||||||
instructions: mainRoute.instructions,
|
instructions: mainRoute.instructions,
|
||||||
|
elevationSummary: mainRoute.elevationSummary,
|
||||||
alternatives: altRoutes
|
alternatives: altRoutes
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -214,6 +219,107 @@ export class MapsService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enriches turn-by-turn routing instructions with steep slope / incline warnings
|
||||||
|
*/
|
||||||
|
private enrichInstructionsWithSlopeAnalysis(instructions: any[], coords: [number, number][]) {
|
||||||
|
if (!instructions || !coords || coords.length < 2) {
|
||||||
|
return {
|
||||||
|
enrichedInstructions: instructions,
|
||||||
|
slopeSummary: { totalAscentMeters: 0, totalDescentMeters: 0, maxInclinePercent: 0, maxDeclinePercent: 0, steepWarningsCount: 0, steepWarnings: [] }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let totalAscent = 0;
|
||||||
|
let totalDescent = 0;
|
||||||
|
let maxInclinePercent = 0;
|
||||||
|
let maxDeclinePercent = 0;
|
||||||
|
const steepWarnings: any[] = [];
|
||||||
|
|
||||||
|
const enrichedInstructions = instructions.map((inst: any) => {
|
||||||
|
const interval = inst.interval || [0, 0];
|
||||||
|
const startIdx = Math.min(interval[0], coords.length - 1);
|
||||||
|
const endIdx = Math.min(interval[1], coords.length - 1);
|
||||||
|
|
||||||
|
if (startIdx < endIdx) {
|
||||||
|
const startCoord = coords[startIdx]; // [lng, lat]
|
||||||
|
const endCoord = coords[endIdx];
|
||||||
|
|
||||||
|
const startElev = this.estimateElevation(startCoord[1], startCoord[0]);
|
||||||
|
const endElev = this.estimateElevation(endCoord[1], endCoord[0]);
|
||||||
|
const elevDiff = endElev - startElev;
|
||||||
|
const dist = inst.distance || this.haversineDistance(startCoord[1], startCoord[0], endCoord[1], endCoord[0]) || 1;
|
||||||
|
|
||||||
|
if (elevDiff > 0) totalAscent += elevDiff;
|
||||||
|
else totalDescent += Math.abs(elevDiff);
|
||||||
|
|
||||||
|
const slopePercent = Math.round((elevDiff / Math.max(20, dist)) * 100);
|
||||||
|
|
||||||
|
if (slopePercent > maxInclinePercent) maxInclinePercent = slopePercent;
|
||||||
|
if (slopePercent < maxDeclinePercent) maxDeclinePercent = slopePercent;
|
||||||
|
|
||||||
|
let warning_ar: string | null = null;
|
||||||
|
|
||||||
|
if (slopePercent >= 9) {
|
||||||
|
warning_ar = `⚠️ تنبيه: صعود حاد (+${slopePercent}%)`;
|
||||||
|
steepWarnings.push({ text: inst.text, slopePercent, type: 'incline', street: inst.street_name });
|
||||||
|
} else if (slopePercent <= -9) {
|
||||||
|
warning_ar = `⚠️ تنبيه: منحدر شديد (${slopePercent}%) - خفف السرعة`;
|
||||||
|
steepWarnings.push({ text: inst.text, slopePercent, type: 'decline', street: inst.street_name });
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...inst,
|
||||||
|
slopePercent,
|
||||||
|
elevationChangeMeters: Math.round(elevDiff),
|
||||||
|
slopeWarning: warning_ar || undefined,
|
||||||
|
text: warning_ar ? `${inst.text} (${warning_ar})` : inst.text
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return inst;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
enrichedInstructions,
|
||||||
|
slopeSummary: {
|
||||||
|
totalAscentMeters: Math.round(totalAscent),
|
||||||
|
totalDescentMeters: Math.round(totalDescent),
|
||||||
|
maxInclinePercent: Math.round(maxInclinePercent),
|
||||||
|
maxDeclinePercent: Math.round(maxDeclinePercent),
|
||||||
|
steepWarningsCount: steepWarnings.length,
|
||||||
|
steepWarnings
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private estimateElevation(lat: number, lng: number): number {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
private haversineDistance(lat1: number, lon1: number, lat2: number, lon2: number): number {
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract the most significant street name from instructions to name the route.
|
* Extract the most significant street name from instructions to name the route.
|
||||||
*/
|
*/
|
||||||
@@ -268,18 +374,14 @@ export class MapsService {
|
|||||||
let dlng = ((result & 1) ? ~(result >> 1) : (result >> 1));
|
let dlng = ((result & 1) ? ~(result >> 1) : (result >> 1));
|
||||||
lng += dlng;
|
lng += dlng;
|
||||||
|
|
||||||
points.push([lng * 1e-5, lat * 1e-5]); // Note: GraphHopper polyline is [lng, lat] usually or lat, lng depending on config.
|
points.push([lng * 1e-5, lat * 1e-5]);
|
||||||
// GH polyline standard is [lat, lng] but we need [lng, lat] for PostGIS GeoJSON Coordinates.
|
|
||||||
// Let's verify: GraphHopper default polyline is Lat,Lng.
|
|
||||||
}
|
}
|
||||||
return points;
|
return points;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMapConfig() {
|
async getMapConfig() {
|
||||||
// Return basic map configuration for clients
|
|
||||||
// إرجاع إعدادات الخريطة للواجهة الأمامية
|
|
||||||
return {
|
return {
|
||||||
center: [31.95, 35.91], // Amman, Jordan
|
center: [31.95, 35.91],
|
||||||
zoom: 12,
|
zoom: 12,
|
||||||
tileServerUrl: this.configService.get('TILE_SERVER_URL', 'http://localhost:3001'),
|
tileServerUrl: this.configService.get('TILE_SERVER_URL', 'http://localhost:3001'),
|
||||||
};
|
};
|
||||||
|
|||||||
+80
-9
@@ -392,18 +392,89 @@ function App() {
|
|||||||
|
|
||||||
{/* Route Summary Card */}
|
{/* Route Summary Card */}
|
||||||
{routeData && (
|
{routeData && (
|
||||||
<div className="route-summary glass-morphism" style={{ marginTop: '15px', padding: '12px', borderRadius: '8px', background: 'rgba(15, 23, 42, 0.6)' }}>
|
<div className="route-summary glass-morphism" style={{ marginTop: '15px', padding: '14px', borderRadius: '10px', background: 'rgba(15, 23, 42, 0.75)', border: '1px solid rgba(56, 189, 248, 0.3)' }}>
|
||||||
<h4 style={{ margin: '0 0 8px 0', fontSize: '0.9rem', color: '#38bdf8' }}>Route Overview / تفاصيل المسار</h4>
|
<h4 style={{ margin: '0 0 10px 0', fontSize: '0.92rem', color: '#38bdf8', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '0.85rem', marginBottom: '4px' }}>
|
<span>{routeData.routeName || 'تفاصيل المسار'}</span>
|
||||||
<span style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
<span style={{ fontSize: '0.75rem', background: 'rgba(56, 189, 248, 0.15)', color: '#38bdf8', padding: '2px 8px', borderRadius: 999 }}>
|
||||||
<Gauge size={13} color="#38bdf8" />
|
{routeData.tags ? routeData.tags[0] : 'FASTEST'}
|
||||||
{(Number(routeData.distance || 0) / 1000).toFixed(1)} km
|
|
||||||
</span>
|
</span>
|
||||||
<span style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
</h4>
|
||||||
<Clock size={13} color="#22c55e" />
|
|
||||||
{Math.round(Number(routeData.time || routeData.duration || 0) / 60000)} min
|
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '0.85rem', marginBottom: '8px' }}>
|
||||||
|
<span style={{ display: 'flex', alignItems: 'center', gap: '4px', color: '#e2e8f0' }}>
|
||||||
|
<Gauge size={14} color="#38bdf8" />
|
||||||
|
{(Number(routeData.distance || 0) / 1000).toFixed(1)} كم
|
||||||
|
</span>
|
||||||
|
<span style={{ display: 'flex', alignItems: 'center', gap: '4px', color: '#4ade80' }}>
|
||||||
|
<Clock size={14} color="#22c55e" />
|
||||||
|
{Math.round(Number(routeData.duration || 0) / 60)} دقيقة
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Elevation & Slope Summary */}
|
||||||
|
{routeData.elevationSummary && (
|
||||||
|
<div style={{ marginTop: 10, paddingTop: 8, borderTop: '1px solid rgba(255,255,255,0.08)', fontSize: '0.78rem' }}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', color: '#94a3b8', marginBottom: 4 }}>
|
||||||
|
<span>📈 إجمالي الصعود: <strong style={{ color: '#4ade80' }}>+{routeData.elevationSummary.totalAscentMeters}م</strong></span>
|
||||||
|
<span>📉 إجمالي الهبوط: <strong style={{ color: '#38bdf8' }}>-{routeData.elevationSummary.totalDescentMeters}م</strong></span>
|
||||||
|
</div>
|
||||||
|
{(routeData.elevationSummary.maxInclinePercent > 0 || routeData.elevationSummary.maxDeclinePercent < 0) && (
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', color: '#94a3b8' }}>
|
||||||
|
<span>أقصى صعود: <strong style={{ color: '#fbbf24' }}>+{routeData.elevationSummary.maxInclinePercent}%</strong></span>
|
||||||
|
<span>أقصى انحدار: <strong style={{ color: '#f87171' }}>{routeData.elevationSummary.maxDeclinePercent}%</strong></span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Steep Slope Warnings Alert */}
|
||||||
|
{routeData.elevationSummary?.steepWarnings && routeData.elevationSummary.steepWarnings.length > 0 && (
|
||||||
|
<div style={{
|
||||||
|
marginTop: 10,
|
||||||
|
padding: '8px 10px',
|
||||||
|
background: 'rgba(239, 68, 68, 0.15)',
|
||||||
|
border: '1px solid rgba(239, 68, 68, 0.35)',
|
||||||
|
borderRadius: 8,
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
color: '#fca5a5'
|
||||||
|
}}>
|
||||||
|
<div style={{ fontWeight: 700, display: 'flex', alignItems: 'center', gap: 6, marginBottom: 4 }}>
|
||||||
|
<AlertTriangle size={13} color="#ef4444" />
|
||||||
|
<span>تحذير تضاريسي للمركبات الثقيلة:</span>
|
||||||
|
</div>
|
||||||
|
{routeData.elevationSummary.steepWarnings.slice(0, 2).map((w: any, idx: number) => (
|
||||||
|
<div key={idx} style={{ marginTop: 2 }}>
|
||||||
|
• {w.street ? `شارع ${w.street}: ` : ''}{w.slopePercent > 0 ? `صعود حاد (+${w.slopePercent}%)` : `منحدر جبلي شديد (${w.slopePercent}%)`}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Turn by turn expandable instructions */}
|
||||||
|
{routeData.instructions && routeData.instructions.length > 0 && (
|
||||||
|
<details style={{ marginTop: 10, fontSize: '0.75rem', color: '#cbd5e1' }}>
|
||||||
|
<summary style={{ cursor: 'pointer', color: '#38bdf8', fontWeight: 600, padding: '4px 0' }}>
|
||||||
|
عرض خطوات المسار بالتفصيل ({routeData.instructions.length} خطوة)
|
||||||
|
</summary>
|
||||||
|
<div style={{ maxHeight: 180, overflowY: 'auto', marginTop: 6, paddingRight: 4, display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||||
|
{routeData.instructions.map((inst: any, idx: number) => (
|
||||||
|
<div key={idx} style={{
|
||||||
|
padding: '4px 6px',
|
||||||
|
borderRadius: 4,
|
||||||
|
background: inst.slopeWarning ? 'rgba(245, 158, 11, 0.12)' : 'rgba(255,255,255,0.03)',
|
||||||
|
borderRight: inst.slopeWarning ? '3px solid #f59e0b' : '1px solid transparent'
|
||||||
|
}}>
|
||||||
|
<div style={{ fontWeight: inst.slopeWarning ? 700 : 400, color: inst.slopeWarning ? '#fbbf24' : '#e2e8f0' }}>
|
||||||
|
{idx + 1}. {inst.text}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '0.7rem', color: '#94a3b8' }}>
|
||||||
|
{inst.distance ? `${Math.round(inst.distance)}م` : ''} {inst.slopePercent ? `| انحدار: ${inst.slopePercent}%` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -212,65 +212,103 @@ export const ExecutiveShowcase: React.FC = () => {
|
|||||||
display: 'inline-flex',
|
display: 'inline-flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 8,
|
gap: 8,
|
||||||
background: 'rgba(79, 70, 229, 0.07)',
|
background: 'linear-gradient(135deg, rgba(79, 70, 229, 0.1), rgba(6, 182, 212, 0.1))',
|
||||||
border: '1px solid rgba(79, 70, 229, 0.18)',
|
border: '1px solid rgba(79, 70, 229, 0.25)',
|
||||||
padding: '6px 16px',
|
padding: '7px 20px',
|
||||||
borderRadius: 999,
|
borderRadius: 999,
|
||||||
color: '#4f46e5',
|
color: '#4338ca',
|
||||||
fontSize: '0.84rem',
|
fontSize: '0.86rem',
|
||||||
fontWeight: 700,
|
fontWeight: 800,
|
||||||
marginBottom: 20
|
marginBottom: 22,
|
||||||
|
boxShadow: '0 2px 10px rgba(79, 70, 229, 0.08)'
|
||||||
}}>
|
}}>
|
||||||
<Sparkles size={16} /> البديل الوطني المتكامل لمنظومات إزري (ArcGIS) في الأردن والشرق الأوسط
|
<Flame size={17} color="#f97316" /> سؤال السيادة الوطنية لعام 2026: هل نمتلك خرائطنا أم نستأجرها؟
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 style={{
|
<h2 style={{
|
||||||
fontSize: 'clamp(1.7rem, 3.4vw, 2.6rem)',
|
fontSize: 'clamp(1.8rem, 3.6vw, 2.75rem)',
|
||||||
fontWeight: 800,
|
fontWeight: 900,
|
||||||
lineHeight: 1.32,
|
lineHeight: 1.3,
|
||||||
margin: '0 auto 18px auto',
|
margin: '0 auto 20px auto',
|
||||||
maxWidth: 920,
|
maxWidth: 960,
|
||||||
color: '#0f172a',
|
color: '#0f172a',
|
||||||
letterSpacing: '-0.025em'
|
letterSpacing: '-0.03em'
|
||||||
}}>
|
}}>
|
||||||
منصة استخبارات جغرافية وطنية وتوليد ذاتي للشوارع والتضاريس التكتيكية
|
معركة السيادة المكانية: كيف نكسر احتكار إزري ونمتلك محرك الخرائط والذكاء التكتيكي الأسرع في المنطقة؟
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<p style={{
|
<p style={{
|
||||||
fontSize: '1.02rem',
|
fontSize: '1.08rem',
|
||||||
color: '#475569',
|
color: '#334155',
|
||||||
maxWidth: 820,
|
maxWidth: 860,
|
||||||
margin: '0 auto 32px auto',
|
margin: '0 auto 28px auto',
|
||||||
lineHeight: 1.8,
|
lineHeight: 1.85,
|
||||||
fontWeight: 400
|
fontWeight: 500
|
||||||
}}>
|
}}>
|
||||||
منظومة سيادية متكاملة تجمع بين <strong>محرك خرائط عالي الأداء معزول سحابياً (Air-Gapped)</strong>، وقدرات استخبارات تكتيكية متقدمة تشمل <strong>دراسة الأرض ثلاثية الأبعاد (21×21 DEM)</strong>، فحص الجروف الصخرية، خط النظر الكروي، مسارات المدفعية، وحساب حقول الألغام ومهابط الطيران.
|
استثمار وطني استراتيجي مزدوج: <strong>تطبيق نقل وأساطيل ذكي في الواجهة المدنية</strong>، و<strong>محرك خرائط عسكري سيادي في الخلفية (C4ISR & GEOINT)</strong> يكتشف الشوارع والمسارب الوعرة تلقائياً من حركة الآليات، ويمنح القوات المسلحة تفوقاً تكتيكياً كاملاً داخل خوادم معزولة تماماً (Air-Gapped).
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
{/* 3 Strategic Shock Cards (حقائق تصدم صانع القرار) */}
|
||||||
|
<div style={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))',
|
||||||
|
gap: 16,
|
||||||
|
maxWidth: 1040,
|
||||||
|
margin: '0 auto 32px auto',
|
||||||
|
textAlign: 'right'
|
||||||
|
}}>
|
||||||
|
<div className="apple-card" style={{ background: '#ffffff', padding: '18px 20px', borderRadius: 16, border: '1px solid #fee2e2', borderRight: '4px solid #ef4444' }}>
|
||||||
|
<div style={{ fontWeight: 800, color: '#dc2626', fontSize: '0.92rem', marginBottom: 4, display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
|
<ShieldAlert size={16} /> 1. صدمة التبعية والسرية
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '0.82rem', color: '#64748b', lineHeight: 1.6 }}>
|
||||||
|
لماذا تمر بيانات تحركاتنا ومواقعنا عبر خوادم إزري السحابية في أمريكا بينما يمكننا تشغيلها بالكامل داخل سيرفرات المركز الجغرافي المعزولة؟
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="apple-card" style={{ background: '#ffffff', padding: '18px 20px', borderRadius: 16, border: '1px solid #fef3c7', borderRight: '4px solid #f59e0b' }}>
|
||||||
|
<div style={{ fontWeight: 800, color: '#d97706', fontSize: '0.92rem', marginBottom: 4, display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
|
<Zap size={16} /> 2. الخريطة التي ترسم نفسها
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '0.82rem', color: '#64748b', lineHeight: 1.6 }}>
|
||||||
|
حركة دوريات وشاحنات الجيش تولد مسارات الأودية والحدود والصحراء آلياً فور عبورها، دون انتظار طائرات مسح أو صور أقمار صناعية دورية!
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="apple-card" style={{ background: '#ffffff', padding: '18px 20px', borderRadius: 16, border: '1px solid #dbeafe', borderRight: '4px solid #3b82f6' }}>
|
||||||
|
<div style={{ fontWeight: 800, color: '#2563eb', fontSize: '0.92rem', marginBottom: 4, display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
|
<Crosshair size={16} /> 3. درع العمليات والرماية التكتيكي
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '0.82rem', color: '#64748b', lineHeight: 1.6 }}>
|
||||||
|
أول محرك عربي مدمج به: رماية المدفعية البالستية، كشف الميدان 360°، تحذيرات المنحدرات الجبلية، وحساب زوايا الموقع بالميللي العسكري.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Quick Metrics Bar (Light Apple Glass) */}
|
{/* Quick Metrics Bar (Light Apple Glass) */}
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gridTemplateColumns: 'repeat(auto-fit, minmax(210px, 1fr))',
|
gridTemplateColumns: 'repeat(auto-fit, minmax(210px, 1fr))',
|
||||||
gap: 16,
|
gap: 14,
|
||||||
maxWidth: 1040,
|
maxWidth: 1040,
|
||||||
margin: '0 auto'
|
margin: '0 auto'
|
||||||
}}>
|
}}>
|
||||||
<div className="apple-card" style={{ background: '#ffffff', padding: '20px 16px', borderRadius: 16, border: '1px solid #e2e8f0', boxShadow: '0 4px 16px rgba(0,0,0,0.03)' }}>
|
<div className="apple-card" style={{ background: '#ffffff', padding: '18px 16px', borderRadius: 16, border: '1px solid #e2e8f0', boxShadow: '0 4px 16px rgba(0,0,0,0.03)' }}>
|
||||||
<div style={{ fontSize: '1.85rem', fontWeight: 900, color: '#4f46e5', letterSpacing: '-0.02em' }}>441 نقطة</div>
|
<div style={{ fontSize: '1.85rem', fontWeight: 900, color: '#4f46e5', letterSpacing: '-0.02em' }}>60 FPS</div>
|
||||||
<div style={{ fontSize: '0.84rem', fontWeight: 600, color: '#64748b', marginTop: 4 }}>مسح طبوغرافي عالي الدقة (21×21)</div>
|
|
||||||
</div>
|
|
||||||
<div className="apple-card" style={{ background: '#ffffff', padding: '20px 16px', borderRadius: 16, border: '1px solid #e2e8f0', boxShadow: '0 4px 16px rgba(0,0,0,0.03)' }}>
|
|
||||||
<div style={{ fontSize: '1.85rem', fontWeight: 900, color: '#059669', letterSpacing: '-0.02em' }}>60 FPS</div>
|
|
||||||
<div style={{ fontSize: '0.84rem', fontWeight: 600, color: '#64748b', marginTop: 4 }}>رندرة متجهات فورية (GPU Tiles)</div>
|
<div style={{ fontSize: '0.84rem', fontWeight: 600, color: '#64748b', marginTop: 4 }}>رندرة متجهات فورية (GPU Tiles)</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="apple-card" style={{ background: '#ffffff', padding: '20px 16px', borderRadius: 16, border: '1px solid #e2e8f0', boxShadow: '0 4px 16px rgba(0,0,0,0.03)' }}>
|
<div className="apple-card" style={{ background: '#ffffff', padding: '18px 16px', borderRadius: 16, border: '1px solid #e2e8f0', boxShadow: '0 4px 16px rgba(0,0,0,0.03)' }}>
|
||||||
<div style={{ fontSize: '1.85rem', fontWeight: 900, color: '#d97706', letterSpacing: '-0.02em' }}>100%</div>
|
<div style={{ fontSize: '1.85rem', fontWeight: 900, color: '#059669', letterSpacing: '-0.02em' }}>100%</div>
|
||||||
<div style={{ fontSize: '0.84rem', fontWeight: 600, color: '#64748b', marginTop: 4 }}>معزولة أمنياً (Air-Gapped On-Prem)</div>
|
<div style={{ fontSize: '0.84rem', fontWeight: 600, color: '#64748b', marginTop: 4 }}>معزولة أمنياً (Air-Gapped On-Prem)</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="apple-card" style={{ background: '#ffffff', padding: '20px 16px', borderRadius: 16, border: '1px solid #e2e8f0', boxShadow: '0 4px 16px rgba(0,0,0,0.03)' }}>
|
<div className="apple-card" style={{ background: '#ffffff', padding: '18px 16px', borderRadius: 16, border: '1px solid #e2e8f0', boxShadow: '0 4px 16px rgba(0,0,0,0.03)' }}>
|
||||||
<div style={{ fontSize: '1.85rem', fontWeight: 900, color: '#0284c7', letterSpacing: '-0.02em' }}>$0</div>
|
<div style={{ fontSize: '1.85rem', fontWeight: 900, color: '#d97706', letterSpacing: '-0.02em' }}>$0</div>
|
||||||
<div style={{ fontSize: '0.84rem', fontWeight: 600, color: '#64748b', marginTop: 4 }}>تكاليف تراخيص سنوية أو قيود مستخدمين</div>
|
<div style={{ fontSize: '0.84rem', fontWeight: 600, color: '#64748b', marginTop: 4 }}>تكاليف تراخيص سنوية أو قيود مستخدمين</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="apple-card" style={{ background: '#ffffff', padding: '18px 16px', borderRadius: 16, border: '1px solid #e2e8f0', boxShadow: '0 4px 16px rgba(0,0,0,0.03)' }}>
|
||||||
|
<div style={{ fontSize: '1.85rem', fontWeight: 900, color: '#0284c7', letterSpacing: '-0.02em' }}>3 دول</div>
|
||||||
|
<div style={{ fontSize: '0.84rem', fontWeight: 600, color: '#64748b', marginTop: 4 }}>الأردن 🇯🇴 • سوريا 🇸🇾 • مصر 🇪🇬</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
Reference in New Issue
Block a user