feat: integrate CocoaPods for macos tactical app and update environment configurations
This commit is contained in:
+129
-104
@@ -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]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user