feat(routing): enable elevation profile and steep slope/incline warnings in routing API and frontend

This commit is contained in:
Hamza-Ayed
2026-08-18 12:10:32 +03:00
parent 06543ee8e0
commit 57e3da7ea4
12 changed files with 599 additions and 100 deletions
+1
View File
@@ -19,6 +19,7 @@ export declare class MapsController {
points: any;
bbox: any;
instructions: any;
elevationSummary: any;
alternatives: any;
}>;
getConfig(): Promise<{
+4
View File
@@ -28,8 +28,12 @@ export declare class MapsService {
points: any;
bbox: any;
instructions: any;
elevationSummary: any;
alternatives: any;
}>;
private enrichInstructionsWithSlopeAnalysis;
private estimateElevation;
private haversineDistance;
private getRouteName;
private decodePolyline;
getMapConfig(): Promise<{
+91 -1
View File
@@ -158,6 +158,7 @@ let MapsService = class MapsService {
const pTrafficFactor = this.trafficGrid.getTrafficFactor(pCoords, hr, dow);
const pDuration = Math.round((p.time / 1000) * pTrafficFactor);
const routeName = this.getRouteName(p.instructions);
const { enrichedInstructions, slopeSummary } = this.enrichInstructionsWithSlopeAnalysis(p.instructions, pCoords);
const tags = [];
if (index === 0)
tags.push('FASTEST');
@@ -175,7 +176,8 @@ let MapsService = class MapsService {
duration: pDuration,
points: p.points,
bbox: p.bbox,
instructions: steps ? p.instructions : undefined
instructions: steps ? enrichedInstructions : undefined,
elevationSummary: slopeSummary
};
});
const mainRoute = processedPaths[0];
@@ -191,6 +193,7 @@ let MapsService = class MapsService {
points: mainRoute.points,
bbox: mainRoute.bbox,
instructions: mainRoute.instructions,
elevationSummary: mainRoute.elevationSummary,
alternatives: altRoutes
};
}
@@ -200,6 +203,93 @@ let MapsService = class MapsService {
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) {
if (!instructions || instructions.length === 0)
return null;
File diff suppressed because one or more lines are too long
+5 -5
View File
@@ -4,10 +4,10 @@ import { TacticalService } from './tactical.service';
export declare class TacticalController {
private readonly tacticalService;
constructor(tacticalService: TacticalService);
getLineOfSight(query: LineOfSightQueryDto): Promise<any>;
getLosAlias(query: LineOfSightQueryDto): Promise<any>;
postLineOfSight(body: LineOfSightBodyDto): Promise<any>;
postLosAlias(body: LineOfSightBodyDto): Promise<any>;
getLineOfSight(query: LineOfSightQueryDto): Promise<import("./tactical.service").LineOfSightResponse>;
getLosAlias(query: LineOfSightQueryDto): Promise<import("./tactical.service").LineOfSightResponse>;
postLineOfSight(body: LineOfSightBodyDto): Promise<import("./tactical.service").LineOfSightResponse>;
postLosAlias(body: LineOfSightBodyDto): Promise<import("./tactical.service").LineOfSightResponse>;
calculateArtilleryFireMission(dto: ArtilleryMissionRequestDto): Promise<{
fireMissionId: string;
caliber: string;
@@ -45,5 +45,5 @@ export declare class TacticalController {
count: number;
name: string;
}>;
getScenario(name?: string): Promise<import("./tactical.service").TacticalSymbol[]>;
getScenario(name?: string): Promise<import("./dto/tactical.dto").TacticalSymbolDto[]>;
}
+61 -22
View File
@@ -1,31 +1,70 @@
import { RedisService } from '../common/redis.service';
export interface ArtilleryMissionRequest {
gunLat: number;
gunLng: number;
targetLat: 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;
import { ArtilleryMissionRequestDto, TacticalSymbolDto } from './dto/tactical.dto';
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[];
}
export declare class TacticalService {
private readonly redisService;
private readonly logger;
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;
caliber: string;
muzzleVelocityMs: number;
@@ -57,12 +96,12 @@ export declare class TacticalService {
suitableCount: number;
zones: any[];
}>;
saveScenario(name: string, symbols: TacticalSymbol[]): Promise<{
saveScenario(name: string, symbols: TacticalSymbolDto[]): Promise<{
success: boolean;
count: number;
name: string;
}>;
getScenario(name: string): Promise<TacticalSymbol[]>;
getScenario(name: string): Promise<TacticalSymbolDto[]>;
private haversineDistance;
private calculateBearing;
private estimateElevation;
+173 -19
View File
@@ -19,8 +19,157 @@ let TacticalService = TacticalService_1 = class TacticalService {
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 { 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);
@@ -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));
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));
@@ -45,11 +194,11 @@ let TacticalService = TacticalService_1 = class TacticalService {
highAngleRad = (60 * Math.PI) / 180;
}
const lowAngleDeg = (lowAngleRad * 180) / Math.PI;
const lowAngleMils = (lowAngleDeg * (6400 / 360));
const lowAngleMils = lowAngleDeg * (6400 / 360);
const highAngleDeg = (highAngleRad * 180) / Math.PI;
const highAngleMils = (highAngleDeg * (6400 / 360));
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 apexHeightMeters = gunTotalElev + Math.pow(v0 * Math.sin(lowAngleRad), 2) / (2 * g);
const trajectoryPoints = [];
const samples = 50;
let hasCrestClearance = true;
@@ -60,7 +209,7 @@ let TacticalService = TacticalService_1 = class TacticalService {
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 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;
@@ -73,7 +222,7 @@ let TacticalService = TacticalService_1 = class TacticalService {
projectileAltMeters: Math.round(projectileAlt),
deficitMeters: Math.round(Math.abs(clearance)),
lat,
lng
lng,
};
}
}
@@ -83,7 +232,7 @@ let TacticalService = TacticalService_1 = class TacticalService {
lng,
terrainElevation: Math.round(terrainElev),
projectileAltitude: Math.round(projectileAlt),
clearanceMeters: Math.round(clearance)
clearanceMeters: Math.round(clearance),
});
}
return {
@@ -108,7 +257,7 @@ let TacticalService = TacticalService_1 = class TacticalService {
},
hasCrestClearance,
criticalObstacle,
trajectoryPoints
trajectoryPoints,
};
}
async assessHelicopterLandingZones(lat, lng, radiusMeters = 3000) {
@@ -116,7 +265,7 @@ let TacticalService = TacticalService_1 = class TacticalService {
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);
const hlzLat = lat + dLat;
@@ -124,7 +273,9 @@ let TacticalService = TacticalService_1 = class TacticalService {
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 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}`,
@@ -136,7 +287,7 @@ let TacticalService = TacticalService_1 = 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),
});
}
return {
@@ -144,8 +295,8 @@ let TacticalService = TacticalService_1 = 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,
};
}
async saveScenario(name, symbols) {
@@ -163,8 +314,10 @@ let TacticalService = TacticalService_1 = class TacticalService {
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);
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) {
@@ -172,8 +325,9 @@ let TacticalService = TacticalService_1 = 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;
}
estimateElevation(lat, lng) {
if (lng < 35.6 && lat < 32.2 && lat > 31.0) {
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+110 -8
View File
@@ -164,13 +164,16 @@ export class MapsService {
const baseDuration = route.time / 1000;
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 pCoords = this.decodePolyline(p.points);
const pTrafficFactor = this.trafficGrid.getTrafficFactor(pCoords, hr, dow);
const pDuration = Math.round((p.time / 1000) * pTrafficFactor);
const routeName = this.getRouteName(p.instructions);
// Analyze slopes and enrich instructions
const { enrichedInstructions, slopeSummary } = this.enrichInstructionsWithSlopeAnalysis(p.instructions, pCoords);
// Tags assignment
const tags: string[] = [];
if (index === 0) tags.push('FASTEST');
@@ -187,7 +190,8 @@ export class MapsService {
duration: pDuration,
points: p.points,
bbox: p.bbox,
instructions: steps ? p.instructions : undefined
instructions: steps ? enrichedInstructions : undefined,
elevationSummary: slopeSummary
};
});
@@ -205,6 +209,7 @@ export class MapsService {
points: mainRoute.points,
bbox: mainRoute.bbox,
instructions: mainRoute.instructions,
elevationSummary: mainRoute.elevationSummary,
alternatives: altRoutes
};
} 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.
*/
@@ -268,18 +374,14 @@ export class MapsService {
let dlng = ((result & 1) ? ~(result >> 1) : (result >> 1));
lng += dlng;
points.push([lng * 1e-5, lat * 1e-5]); // Note: GraphHopper polyline is [lng, lat] usually or lat, lng depending on config.
// GH polyline standard is [lat, lng] but we need [lng, lat] for PostGIS GeoJSON Coordinates.
// Let's verify: GraphHopper default polyline is Lat,Lng.
points.push([lng * 1e-5, lat * 1e-5]);
}
return points;
}
async getMapConfig() {
// Return basic map configuration for clients
// إرجاع إعدادات الخريطة للواجهة الأمامية
return {
center: [31.95, 35.91], // Amman, Jordan
center: [31.95, 35.91],
zoom: 12,
tileServerUrl: this.configService.get('TILE_SERVER_URL', 'http://localhost:3001'),
};