diff --git a/apps/api/src/maps/maps.service.ts b/apps/api/src/maps/maps.service.ts index 074c979..8bc8334 100644 --- a/apps/api/src/maps/maps.service.ts +++ b/apps/api/src/maps/maps.service.ts @@ -315,39 +315,29 @@ export class MapsService { const localizedBaseText = this.localizeInstructionToArabic(inst.text); if (startIdx < endIdx) { - let stepAscent = 0; - let stepDescent = 0; + 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); - for (let i = startIdx; i < endIdx; i++) { - const c1 = coords[i]; - const c2 = coords[i + 1]; - const e1 = this.estimateElevation(c1[1], c1[0]); - const e2 = this.estimateElevation(c2[1], c2[0]); - const dDiff = e2 - e1; + if (elevDiff > 0.3) totalAscent += elevDiff; + else if (elevDiff < -0.3) totalDescent += Math.abs(elevDiff); - if (dDiff > 0) stepAscent += dDiff; - else stepDescent += Math.abs(dDiff); - } - - totalAscent += stepAscent; - totalDescent += stepDescent; - - const dist = Math.max(40, inst.distance || 1); - const netStepDiff = stepAscent - stepDescent; - - // Realistic slope calculation calibrated for highway/urban gradients - const calculatedSlope = Math.round((netStepDiff / dist) * 100); - const slopePercent = Math.max(-14, Math.min(14, calculatedSlope)); + // Grade percentage: (rise / run) * 100 + const rawSlope = (elevDiff / dist) * 100; + const slopePercent = Math.max(-16, Math.min(16, Math.round(rawSlope))); if (slopePercent > maxInclinePercent) maxInclinePercent = slopePercent; if (slopePercent < maxDeclinePercent) maxDeclinePercent = slopePercent; - // Warning only for truly steep inclines/declines (10% or higher) + // Warning only for steep grades (6% or higher is standard civil road warning threshold) let warning_ar: string | null = null; - if (slopePercent >= 10) { + if (slopePercent >= 6) { warning_ar = `⚠️ تنبيه: صعود حاد (+${slopePercent}%)`; steepWarnings.push({ text: localizedBaseText, slopePercent, type: 'incline', street: inst.street_name }); - } else if (slopePercent <= -10) { + } else if (slopePercent <= -6) { warning_ar = `⚠️ تنبيه: منحدر شديد (${slopePercent}%) - خفف السرعة`; steepWarnings.push({ text: localizedBaseText, slopePercent, type: 'decline', street: inst.street_name }); } @@ -355,7 +345,7 @@ export class MapsService { return { ...inst, slopePercent, - elevationChangeMeters: Math.round(netStepDiff), + elevationChangeMeters: Math.round(elevDiff), slopeWarning: warning_ar || undefined, text: warning_ar ? `${localizedBaseText} (${warning_ar})` : localizedBaseText }; @@ -608,56 +598,70 @@ export class MapsService { } /** - * High-Precision Topographic Elevation Model for Jordan - * Continuous multi-scale terrain incorporating valley relief, ridge crests, and natural drainage slopes. + * 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. Regional Base Elevation Surface (Western Rift to Eastern Desert) - let baseElev = 750; - - // Rift Valley Depression (Jordan Valley / Dead Sea) - if (lng < 35.65) { - const riftDist = Math.max(0, Math.min(1, (35.65 - lng) / 0.15)); - const riftBottom = lat < 31.9 ? -420 : (lat < 32.3 ? -220 : -150); - const highlandEdge = lat > 32.2 ? 1000 : (lat > 31.5 ? 900 : 1300); - baseElev = highlandEdge - riftDist * (highlandEdge - riftBottom); - } - // Western Highlands (Ajloun, Balqa, West Amman, Tafila, Dana, Petra) - else if (lng >= 35.65 && lng < 35.88) { - if (lat >= 32.25) baseElev = 950 + (lat - 32.25) * 150; // Ajloun / Jerash Highlands (950m - 1150m) - else if (lat >= 31.85) baseElev = 920 + (lng - 35.75) * 400; // Balqa / West Amman (850m - 1040m) - else if (lat >= 30.2) baseElev = 1100 + (31.85 - lat) * 150; // Shobak / Dana / Petra (1100m - 1550m) - else baseElev = 800 - (30.2 - lat) * 700; // South towards Aqaba - } - // Central Urban Basin (East Amman, Zarqa, Rusaifa, Madaba, Irbid Plateau) - else if (lng >= 35.88 && lng < 36.25) { - if (lat >= 32.35) { - baseElev = 580 + (lat - 32.35) * 80; // Irbid Plateau - } else if (lat >= 32.0 && lat < 32.35) { - // Zarqa & Sukhna Basin Topography (Parabolic valley profile with natural drainage) - const riverAxisLat = 32.06 + (lng - 36.05) * 0.65; - const distFromRiverKm = Math.abs(lat - riverAxisLat) * 111.32; - // Parabolic rise from riverbed (530m) up to surrounding hills (660m) - const valleyRelief = 120 * (1 - Math.exp(-Math.pow(distFromRiverKm / 1.5, 2))); - const longitudinalSlope = (32.15 - lat) * 60; // Slopes downwards northwards - const localHillWave = Math.sin(lat * 80.0) * 18.0 + Math.cos(lng * 90.0) * 14.0; - baseElev = 540 + valleyRelief + longitudinalSlope + localHillWave; - } else if (lat >= 31.85 && lat < 32.0) { - // Amman 7 Hills & Valleys Topography - const localAmmanHill = Math.sin((lat - 31.95) * 120) * 45 + Math.cos((lng - 35.92) * 120) * 40; - baseElev = 840 - (lng - 35.90) * 350 + localAmmanHill; - } else if (lat < 31.85 && lat >= 31.6) { - baseElev = 740 + (lat - 31.6) * 100; // Airport / Madaba plains - } else { - baseElev = 700; - } - } - // Eastern Desert Plateau (Mafraq, Azraq, Safawi) - else { - baseElev = 640 + (lng - 36.25) * 20 - (lat - 32.0) * 30; + // 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; } - return baseElev; + // 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; } private haversineDistance(lat1: number, lon1: number, lat2: number, lon2: number): number {