feat: integrate CocoaPods for macos tactical app and update environment configurations

This commit is contained in:
Hamza-Ayed
2026-08-21 04:12:13 +03:00
parent 7216d86740
commit 4a7859f4c2
6 changed files with 276 additions and 105 deletions
+129 -104
View File
@@ -70,7 +70,8 @@ export class MapsService {
profile: profile,
locale: locale === 'en' ? 'ar' : locale, // Default to Arabic if not specified or fallback
calc_points: true,
points_encoded: true,
points_encoded: false, // JSON arrays for reliable 3D elevation (SRTM)
elevation: true, // ← SRTM: طلب إحداثيات 3D [lng, lat, elevation] + ascend/descend
instructions: steps || true, // Always request instructions to extract route name
};
@@ -160,7 +161,8 @@ export class MapsService {
const hr = now.getHours();
const dow = now.getDay();
const coords = this.decodePolyline(route.points);
const coords3D = this.extractCoords3D(route.points);
const coords: [number, number][] = coords3D.map(c => [c[0], c[1]]); // 2D for traffic grid
const trafficFactor = this.trafficGrid.getTrafficFactor(coords, hr, dow);
const baseDuration = route.time / 1000;
@@ -172,7 +174,8 @@ export class MapsService {
// Process all paths to add metadata (Real Street Names, Tags, Elevation & Slope Warnings, Eco/Fuel metrics)
const processedPaths = paths.map((p: any, index: number) => {
const pCoords = this.decodePolyline(p.points);
const pCoords3D = this.extractCoords3D(p.points);
const pCoords: [number, number][] = pCoords3D.map(c => [c[0], c[1]]); // 2D for traffic grid
const pTrafficFactor = this.trafficGrid.getTrafficFactor(pCoords, hr, dow);
const pBaseDuration = p.time / 1000;
const pDuration = Math.round(pBaseDuration * pTrafficFactor);
@@ -196,8 +199,12 @@ export class MapsService {
finalRouteName = index === 0 ? 'المسار المباشر الأسرع' : 'مسار بديل عبر الطرق الموازية';
}
// Analyze slopes and enrich instructions
const { enrichedInstructions, slopeSummary } = this.enrichInstructionsWithSlopeAnalysis(p.instructions, pCoords);
// Analyze slopes using REAL 3D elevation from SRTM satellite data
const { enrichedInstructions, slopeSummary } = this.enrichInstructionsWithSlopeAnalysis(p.instructions, pCoords3D);
// Override with GraphHopper's authoritative SRTM ascend/descend values when available
if (typeof p.ascend === 'number') slopeSummary.totalAscentMeters = Math.round(p.ascend);
if (typeof p.descend === 'number') slopeSummary.totalDescentMeters = Math.round(p.descend);
// Calculate Energy, Fuel Consumption & Eco Cost (combining Distance + Ascent/Descent Physics + Traffic/Time Delays)
const ecoMetrics = this.calculateEcoAndFuelMetrics(
@@ -229,7 +236,8 @@ export class MapsService {
tags,
distance: p.distance,
duration: pDuration,
points: p.points,
// Return standard Google-encoded Polyline string
points: this.encodePolyline(pCoords3D.map(c => [c[0], c[1]])),
bbox: p.bbox,
instructions: steps ? enrichedInstructions : undefined,
elevationSummary: slopeSummary,
@@ -290,16 +298,27 @@ export class MapsService {
}
/**
* Enriches turn-by-turn routing instructions with steep slope / incline warnings
* Enriches turn-by-turn routing instructions with calibrated slope / incline warnings.
* Uses smoothed REAL SRTM 3D elevation data to eliminate raster quantization noise.
*/
private enrichInstructionsWithSlopeAnalysis(instructions: any[], coords: [number, number][]) {
if (!instructions || !coords || coords.length < 2) {
private enrichInstructionsWithSlopeAnalysis(instructions: any[], rawCoords3D: [number, number, number][]) {
if (!instructions || !rawCoords3D || rawCoords3D.length < 2) {
return {
enrichedInstructions: instructions,
slopeSummary: { totalAscentMeters: 0, totalDescentMeters: 0, maxInclinePercent: 0, maxDeclinePercent: 0, steepWarningsCount: 0, steepWarnings: [] }
};
}
// 1. Apply Gaussian/Weighted 3-point smoothing on elevation to remove SRTM 30m grid noise
const coords3D: [number, number, number][] = rawCoords3D.map((pt, i, arr) => {
if (i === 0 || i === arr.length - 1) return pt;
const prev = arr[i - 1][2];
const curr = pt[2];
const next = arr[i + 1][2];
const smoothedEle = (prev + 2 * curr + next) / 4;
return [pt[0], pt[1], smoothedEle];
});
let totalAscent = 0;
let totalDescent = 0;
let maxInclinePercent = 0;
@@ -308,44 +327,63 @@ export class MapsService {
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);
const startIdx = Math.min(interval[0], coords3D.length - 1);
const endIdx = Math.min(interval[1], coords3D.length - 1);
// 1. Localize text into 100% fluent Arabic
// Localize text into 100% fluent Arabic
const localizedBaseText = this.localizeInstructionToArabic(inst.text);
if (startIdx < endIdx) {
const c1 = coords[startIdx];
const c2 = coords[endIdx];
const e1 = this.estimateElevation(c1[1], c1[0]);
const e2 = this.estimateElevation(c2[1], c2[0]);
const elevDiff = e2 - e1;
const dist = Math.max(30, inst.distance || this.haversineDistance(c1[1], c1[0], c2[1], c2[0]) || 1);
// Walk segments within this instruction with noise threshold (>= 0.8m)
let stepAscent = 0;
let stepDescent = 0;
for (let i = startIdx; i < endIdx; i++) {
const eDiff = coords3D[i + 1][2] - coords3D[i][2];
if (eDiff >= 0.8) stepAscent += eDiff;
else if (eDiff <= -0.8) stepDescent += Math.abs(eDiff);
}
if (elevDiff > 0.3) totalAscent += elevDiff;
else if (elevDiff < -0.3) totalDescent += Math.abs(elevDiff);
totalAscent += stepAscent;
totalDescent += stepDescent;
// Grade percentage: (rise / run) * 100
const rawSlope = (elevDiff / dist) * 100;
const slopePercent = Math.max(-16, Math.min(16, Math.round(rawSlope)));
// Net elevation change for this instruction
const netElevDiff = coords3D[endIdx][2] - coords3D[startIdx][2];
const dist = Math.max(30, inst.distance || 1);
// Grade percentage: (net rise / run) * 100
const rawSlope = (netElevDiff / dist) * 100;
const clampedRaw = Math.max(-16, Math.min(16, Math.round(rawSlope)));
// Dampen slope severity by 3% as requested (e.g. 13% -> 10%, -13% -> -10%)
let slopePercent = 0;
if (clampedRaw > 0) {
slopePercent = Math.max(0, clampedRaw - 3);
} else if (clampedRaw < 0) {
slopePercent = Math.min(0, clampedRaw + 3);
}
if (slopePercent > maxInclinePercent) maxInclinePercent = slopePercent;
if (slopePercent < maxDeclinePercent) maxDeclinePercent = slopePercent;
// Warning only for steep grades (6% or higher is standard civil road warning threshold)
// Civil road standard: warnings apply to meaningful, sustained grades
// (Distance >= 100m OR significant vertical change >= 12m)
const isSustainedSegment = dist >= 100 || Math.abs(netElevDiff) >= 12;
let warning_ar: string | null = null;
if (slopePercent >= 6) {
warning_ar = `⚠️ تنبيه: صعود حاد (+${slopePercent}%)`;
steepWarnings.push({ text: localizedBaseText, slopePercent, type: 'incline', street: inst.street_name });
} else if (slopePercent <= -6) {
warning_ar = `⚠️ تنبيه: منحدر شديد (${slopePercent}%) - خفف السرعة`;
steepWarnings.push({ text: localizedBaseText, slopePercent, type: 'decline', street: inst.street_name });
if (isSustainedSegment) {
if (slopePercent >= 8) {
warning_ar = `⚠️ تنبيه: صعود حاد (+${slopePercent}%)`;
steepWarnings.push({ text: localizedBaseText, slopePercent, type: 'incline', street: inst.street_name });
} else if (slopePercent <= -8) {
warning_ar = `⚠️ تنبيه: منحدر شديد (${slopePercent}%) - خفف السرعة`;
steepWarnings.push({ text: localizedBaseText, slopePercent, type: 'decline', street: inst.street_name });
}
}
return {
...inst,
slopePercent,
elevationChangeMeters: Math.round(elevDiff),
elevationChangeMeters: Math.round(netElevDiff),
slopeWarning: warning_ar || undefined,
text: warning_ar ? `${localizedBaseText} (${warning_ar})` : localizedBaseText
};
@@ -597,72 +635,7 @@ export class MapsService {
return text;
}
/**
* Universal Analytical Topographic Digital Elevation Model (DEM) for the Entire Map of Jordan.
* Continuous, seamless mathematical surface modeling all provinces, mountain crests, valleys, and plateaus.
*/
private estimateElevation(lat: number, lng: number): number {
// 1. Boundary guard (default fallback for global coordinates outside Jordan)
if (lat < 29.0 || lat > 33.5 || lng < 34.5 || lng > 39.5) {
return 700;
}
// 2. Rift Valley Axis (Wadi Araba, Dead Sea, Jordan Valley)
// The rift axis runs along a slightly tilted meridian (Lng ~35.56 in North, ~35.00 in South)
const riftLng = 35.00 + (lat - 29.5) * (35.56 - 35.00) / (33.0 - 29.5);
const distFromRiftLng = lng - riftLng; // Negative = West of rift, Positive = East of rift
// Elevation along the Rift Valley floor
let riftFloorElev: number;
if (lat >= 32.7) riftFloorElev = -200 + (lat - 32.7) * 200; // Sea of Galilee / Yarmouk (-200m to 0m)
else if (lat >= 31.5) riftFloorElev = -430 + Math.pow((lat - 31.5) / 1.2, 2) * 230; // Dead Sea to Deir Alla (-430m to -200m)
else if (lat >= 30.5) riftFloorElev = -430 + (31.5 - lat) * 450; // Dead Sea south to Gharandal (-430m to +20m)
else riftFloorElev = 20 + (lat - 29.5) * 80; // Wadi Araba to Aqaba (+20m to +100m)
// Crest Elevation of the Eastern Mountain Ridge (Ajloun -> Balqa -> Karak -> Tafila -> Shobak -> Ras En Naqb)
let highlandCrestElev: number;
const crestDistanceDeg = 0.28; // Distance in degrees from Rift axis to the mountain crest (~28 km)
if (lat >= 32.2) highlandCrestElev = 1050 + (lat - 32.2) * 100; // Ajloun / Jerash (1050m - 1200m)
else if (lat >= 31.8) highlandCrestElev = 980 + (lat - 31.8) * 150; // Salt / West Amman (980m - 1040m)
else if (lat >= 31.3) highlandCrestElev = 820 + (lat - 31.3) * 200; // Madaba / Central (820m - 920m)
else if (lat >= 30.7) highlandCrestElev = 1100 + (31.3 - lat) * 400; // Karak / Tafila (1100m - 1340m)
else if (lat >= 29.9) highlandCrestElev = 1450 + (30.7 - lat) * 150; // Dana / Shobak / Ras En Naqb (1450m - 1570m)
else highlandCrestElev = 850 - (29.9 - lat) * 800; // Drop to Aqaba Mountains
// 3. Physical Cross-Section Profile across Jordan (East-West Topography)
let elev: number;
if (distFromRiftLng <= 0) {
// In the Rift or Western escarpment
const wFraction = Math.min(1, Math.abs(distFromRiftLng) / 0.15);
elev = riftFloorElev + (700 - riftFloorElev) * (wFraction * wFraction);
} else if (distFromRiftLng <= crestDistanceDeg) {
// Steep ascent from Rift Valley floor to Mountain Crest
const ascentFraction = distFromRiftLng / crestDistanceDeg; // 0 (Rift) -> 1 (Mountain Crest)
// S-curve steep escarpment (Sigmoid transition)
const sCurve = Math.sin((ascentFraction - 0.5) * Math.PI) * 0.5 + 0.5;
elev = riftFloorElev + (highlandCrestElev - riftFloorElev) * sCurve;
} else {
// East of Mountain Crest: Gentle slope down into the Eastern Plateaus and Basins
const eastDistDeg = distFromRiftLng - crestDistanceDeg;
let easternBasePlateau = 680;
if (lat >= 32.4) easternBasePlateau = 560; // Irbid / Ramtha Plateau
else if (lat >= 31.9) easternBasePlateau = 580; // Amman East / Zarqa Basin
else if (lat >= 31.4) easternBasePlateau = 720; // Airport / Qatranah
else easternBasePlateau = 850; // Maan / Southern Desert Plateau
// Decay from Mountain Crest towards Eastern Base Plateau
const plateauDecay = Math.exp(-eastDistDeg / 0.35);
elev = easternBasePlateau + (highlandCrestElev - easternBasePlateau) * plateauDecay;
// Eastern Desert Azraq depression (Lng 36.8, Lat 31.8)
const azraqDist = Math.hypot((lat - 31.83) * 1.1, (lng - 36.82));
if (azraqDist < 0.6) {
elev -= (1 - azraqDist / 0.6) * 120; // Dips to ~510m in Azraq
}
}
return elev;
}
// estimateElevation REMOVED — replaced by real SRTM satellite data from GraphHopper
private haversineDistance(lat1: number, lon1: number, lat2: number, lon2: number): number {
const R = 6371000;
@@ -736,7 +709,62 @@ export class MapsService {
}
/**
* Manual decoder for Google Polyline algorithm (Server-side spatial matching)
* Extracts 3D coordinates from GraphHopper's JSON points response.
* With points_encoded=false and elevation=true, GH returns:
* { type: "LineString", coordinates: [[lng, lat, ele], ...] }
* Returns: [lng, lat, elevation][] — compatible with all coord consumers.
*/
private extractCoords3D(points: any): [number, number, number][] {
if (!points) return [];
// GH returns { type: "LineString", coordinates: [[lng, lat, ele], ...] }
const rawCoords = points.coordinates || points;
if (!Array.isArray(rawCoords)) return [];
return rawCoords.map((c: number[]) => {
// c = [lng, lat, elevation_meters]
return [c[0], c[1], c[2] || 0] as [number, number, number];
});
}
/**
* Encodes array of [lng, lat] coordinates into a standard Google-encoded polyline string.
*/
private encodePolyline(coords: [number, number][], precision: number = 5): string {
if (!coords || coords.length === 0) return '';
const factor = Math.pow(10, precision);
let output = '';
let prevLat = 0;
let prevLng = 0;
const encodeSignedNumber = (num: number): string => {
let sgn_num = num < 0 ? ~(num << 1) : (num << 1);
let encodeString = '';
while (sgn_num >= 0x20) {
encodeString += String.fromCharCode((0x20 | (sgn_num & 0x1f)) + 63);
sgn_num >>= 5;
}
encodeString += String.fromCharCode(sgn_num + 63);
return encodeString;
};
for (const [lng, lat] of coords) {
const latInt = Math.round(lat * factor);
const lngInt = Math.round(lng * factor);
const dLat = latInt - prevLat;
const dLng = lngInt - prevLng;
prevLat = latInt;
prevLng = lngInt;
output += encodeSignedNumber(dLat);
output += encodeSignedNumber(dLng);
}
return output;
}
/**
* Legacy 2D polyline decoder — kept for any encoded polyline contexts
*/
private decodePolyline(encoded: string): [number, number][] {
const points: [number, number][] = [];
@@ -750,18 +778,15 @@ export class MapsService {
result |= (b & 0x1f) << shift;
shift += 5;
} while (b >= 0x20);
let dlat = ((result & 1) ? ~(result >> 1) : (result >> 1));
lat += dlat;
lat += ((result & 1) ? ~(result >> 1) : (result >> 1));
shift = 0;
result = 0;
shift = 0; result = 0;
do {
b = encoded.charCodeAt(index++) - 63;
result |= (b & 0x1f) << shift;
shift += 5;
} while (b >= 0x20);
let dlng = ((result & 1) ? ~(result >> 1) : (result >> 1));
lng += dlng;
lng += ((result & 1) ? ~(result >> 1) : (result >> 1));
points.push([lng * 1e-5, lat * 1e-5]);
}