Ana içeriğe atla

⚡ Gerçek zamanlı nowcast

Sürücünün bulunduğu segment her zaman canlı radar kaynaklı yıldırım, fırtına ve yağış verisi taşır — model tahmini değil.

🔭 İleriye dönük tahmin

İlerleyen segmentler saatler önceden koşulları planlamak için saatlik NWP tahmin verisiyle zenginleştirilir.

🗺️ Akıllı segmentasyon

Rota, Uber H3 altıgen hücrelerine bölünür. Her hücre kendi ETA’sını ve bağımsız hava sorgusunu alır; böylece veri her zaman taze ve konumsal olarak doğru kalır.

Enroute API nedir?

Enroute API, planlanmış bir rotayı canlı hava takip oturumuna dönüştürür. Rota geometrisini, kalkış saatini ve süresini sağlarsınız — API rotayı coğrafi segmentlere böler, her birine zamanlanmış varış saati ekler ve sonraki her sorguda tüm segmentler için hava verisi döndürür. Tüm yaşam döngüsünü iki uç nokta yönetir:
Uç NoktaAmaç
POST /v1/enroute/registerRota oturumu oluşturur. routeId ve ilk hava durumu görünümünü döndürür.
POST /v1/enroute/weatherSürücünün mevcut konumuyla aktif oturumu sorgular. Güncel hava durumu, konum durumu ve varsa zamanlama değişikliklerini döndürür.
İsteklerdeki ve yanıtlardaki tüm zaman damgaları epoch milisaniyedir (64-bit tam sayı). Asla ISO string veya ondalık saniye göndermeyiniz.

Segmentasyon nasıl çalışır?

Bir rota kaydedildiğinde servis, rota geometrisini (kodlanmış polyline veya koordinat listesi) çözümler ve her noktayı çözünürlük 5’teki Uber H3 altıgen hücresine eşler. Aynı hücreye düşen ardışık noktalar birleştirilir; böylece sıralı benzersiz hücreler listesi — segmentler — elde edilir. Her segment şunları alır:
  • eta — zamanlanmış varış anı: kalkışZamanı + (girişMesafesi / toplamMesafe) × süreSniye şeklinde enterpolasyonla hesaplanır
  • etd — zamanlanmış ayrılış anı (aynı enterpolasyon, çıkışMesafesi kullanılarak)
  • distanceToEntry / distanceToExit — rota başından kümülatif metre değerleri
  • arrivalPoint / departurePoint — rotanın hücreye girdiği ve çıktığı coğrafi koordinatlar
  • h3Address — H3 hücre tanımlayıcısı (hava sorguları için kullanılır)
H3 çözünürlük 5 hücreleri yaklaşık 252 km² kapsar; orta büyüklükte bir ilçeye karşılık gelir. Tipik bir 90 dakikalık otoyol güzergahı 7–10 segmentten oluşur.
Neden segmentasyon? Hava koşulları mekânsal olarak heterojendir. Varış noktanızda yağmur yağarken 80 km geride gökyüzü açık olabilir. Segmentasyon, API’nin yolculuğun her bölümü için doğru hava modeli hücresini sorgulamasına ve bu sorguları sürücünün gerçek ETA’sına göre zamanlamasına olanak tanır — hepsini aynı anda değil.

Nowcast ve Forecast

Yanıt, segmentin zaman ufkuna bağlı olarak iki yapısal olarak farklı hava yükü içerir.

currentSegment — Nowcast + Forecast

Sürücünün şu anda bulunduğu segment (veya kayıt anında ilk segment) nowcast verisi alır: canlı radar ve sensör ağlarından türetilen gerçek zamanlı gözlemler. Bu, mevcut en zengin ve en doğru hava durumu görüntüsüdür. Nowcast, üç özel özet nesne içerir:
NesneKaynakNe söyler
lightningSummaryYıldırım tespit sensörleriFlaş sayıları, tepe akımı, risk endeksi, yaklaşım trendi
thunderstormSummaryRadar fırtına takip hücreleriAktif fırtına listesi, tehdit sınırı yakınlığı, yön, şiddet
precipitationSummaryRadar reflektivitesiMevcut yoğunluk, trend, beklenen başlangıç/bitiş, tahmin maksimumu
Gelecek segmentler neden nowcast kullanamaz? Her üç nowcast servisi de bu konuda farklı ama birbirini destekleyen kısıtlara sahiptir:
  • Yıldırım: Sensör ağları yalnızca gerçekleşmiş flaşları kaydeder. Geleceğe dönük herhangi bir çıktı üretmez; sürücünün 2 saat sonra geçeceği hücre için yıldırım nowcast’i fiziksel olarak mümkün değildir.
  • Fırtına: Radar tabanlı hücre takibi, aktif fırtına hücrelerinin anlık konumunu, hızını ve yönünü izler. Yalnızca mevcut gözleme dayanır; ileriye dönük projeksiyon üretmez. Saatler sonraki bir segment için anlamlı bir tehdit değerlendirmesi yapılamaz.
  • Yağış: Radar gözlemleriyle birlikte çok kısa vadeli bir NWP projeksiyonu da mevcut olabilir; bu projeksiyon precipitationSummary içindeki expectedStartSec / expectedEndSec alanlarını besler. Ancak bu ufuk yalnızca anlık gözlem penceresini biraz aşar ve saatler sonraki bir segmentin koşullarını temsil etmez.
Sonuç olarak: üç servisin ağırlıklı değeri “şu anda, tam burada” sorusuna verdiği yanıtta yatar. İlerleyen segmentler için bu sorunun cevabı anlamsız hale geldiğinden, nowcast yerine çok saatlik ufka sahip NWP tahmin modelleri devreye girer.

upcomingSegments — Yalnızca Forecast

Mevcut segmentten sonraki tüm segmentler NWP (Sayısal Hava Tahmini) saatlik tahmin verisi taşır: sıcaklık, hissedilen sıcaklık, nem, bulut örtüsü, rüzgar hızı/rüzgar hamlesi/yönü, yağış, yağış olasılığı, kar yağışı ve görüş mesafesi.
Kayıt anında, departureTime şimdiden 60 dakika içindeyse mevcut segment için de nowcast verisi alınır. Kalkış daha uzak bir geleceğe aitse, ilk segment için de yalnızca tahmin verisi döndürülür.

Rota kaydı

İstek

POST /v1/enroute/register
Content-Type: application/json
Authorization: Bearer <token>

{
  "origin": { "lat": 41.0052, "lng": 39.7267 },
  "destination": { "lat": 40.4608, "lng": 39.4818 },
  "departureTime": 1776074400000,
  "distanceMeters": 86000,
  "durationSeconds": 5640,
  "geometry": {
    "encodedPolyline": "ywu~FmgpbBrA`AfBtAhD|BlDnCxBdBlBtAfChBjBpA...",
    "polylinePrecision": 5
  }
}
AlanTürZorunluAçıklama
originGeoPointBaşlangıç koordinatı { lat, lng }
destinationGeoPointBitiş koordinatı { lat, lng }
departureTimesayıPlanlanan kalkış zamanı (epoch milisaniye)
distanceMeterssayıToplam rota uzunluğu (metre)
durationSecondssayıTahmini seyahat süresi (saniye)
geometry.encodedPolylinestring✅ (veya coordinates)Google kodlamalı polyline
geometry.polylinePrecisionsayıHassasiyet katsayısı (5 = standart, 6 = yüksek hassasiyet)
geometry.coordinatesGeoPoint[]✅ (veya polyline)Polyline yerine açık koordinat listesi
waypointsGeoPoint[]Opsiyonel ara durak noktaları (bilgi amaçlı)
encodedPolyline (ile polylinePrecision) veya coordinates alanlarından biri mutlaka sağlanmalıdır. İkisi de gönderilmezse 400 hatası döner.

Yanıt

{
  "routeId": "84863286-c5f8-4a2c-9d40-837213e8c38e",
  "expirationTime": 1776087240000,
  "distanceUnit": "m",
  "status": {
    "progressPercentage": 0.0,
    "distanceTraveled": 0.0,
    "remainingDistance": 86000.0,
    "timeElapsedSeconds": 0,
    "estimatedTimeRemainingSeconds": 5640,
    "scheduleDeviationSeconds": 0,
    "expectedSegmentIndex": 0,
    "estimatedArrivalTime": 1776079680000
  },
  "currentSegment": {
    "index": 0,
    "h3Address": "855b8c6bfffffff",
    "eta": 1776074400000,
    "etd": 1776075120000,
    "distanceToEntry": 0.0,
    "distanceToExit": 12200.0,
    "arrivalPoint": { "lat": 41.0052, "lng": 39.7267 },
    "departurePoint": { "lat": 40.9871, "lng": 39.6543 },
    "weatherEvents": {
      "temperature": 18.4,
      "windSpeed": 3.2,
      "windGust": 6.1,
      "windDirection": 270,
      "precipitation": 0.0,
      "precipitationProbability": 12.0,
      "humidity": 64.0,
      "cloudCover": 35.0,
      "visibility": 25000.0,
      "lightningSummary": {
        "windowMinutes": 60,
        "bucketMinutes": 30,
        "totalEventCount": 0,
        "riskIndex": 0.0,
        "riskLevel": "NONE",
        "trend": "STABLE"
      },
      "thunderstormSummary": {
        "windowMinutes": 60,
        "bucketMinutes": 30,
        "summary": {
          "stormCount": 0,
          "activeStorms": 0,
          "insideAnyThreatBoundary": false,
          "riskScore": 0,
          "riskLevel": "NONE",
          "trend": "STABLE"
        }
      },
      "precipitationSummary": {
        "windowMinutes": 60,
        "bucketMinutes": 30,
        "currentIntensity": null,
        "isPrecipitating": false,
        "riskLevel": "NONE",
        "trend": "STABLE"
      }
    }
  },
  "upcomingSegments": [
    {
      "index": 1,
      "h3Address": "855b8c67fffffff",
      "eta": 1776075120000,
      "etd": 1776076560000,
      "distanceToEntry": 12200.0,
      "distanceToExit": 24100.0,
      "weatherEvents": {
        "temperature": 16.1,
        "windSpeed": 5.8,
        "windGust": 12.3,
        "windDirection": 285,
        "precipitation": 1.4,
        "precipitationProbability": 65.0,
        "humidity": 78.0,
        "cloudCover": 85.0,
        "visibility": 12000.0
      }
    }
  ]
}
routeId değerini kaydedin — yolculuk boyunca /weather uç noktasını sorgulamak için gereken tek anahtardır. expirationTime, oturumun ne zaman sona ereceğini gösterir; ancak sürücü zamanlama dışı kalıp segmentler yeniden hesaplandığında bu değer uzatılır (bkz. Zamanlama Dışı).

Rota hava durumu sorgulama

İstek

POST /v1/enroute/weather
Content-Type: application/json
Authorization: Bearer <token>

{
  "routeId": "84863286-c5f8-4a2c-9d40-837213e8c38e",
  "currentLocation": { "lat": 40.9871, "lng": 39.6543 }
}
AlanTürZorunluAçıklama
routeIdUUID string/register’dan dönen oturum tanımlayıcısı
currentLocationGeoPointSürücünün anlık GPS koordinatı

Yanıt

GetRouteWeatherResponse, RouteRegistrationResponse’ı genişletir ve şunları ekler:
AlanTürAçıklama
segmentsRecalculatedbooleanZamanlama dışı kalma nedeniyle segmentler yeniden hesaplandıysa true
warningstringYeniden hesaplamanın neden gerçekleştiğini açıklayan kullanıcı dostu mesaj
Diğer tüm alanlar (routeId, expirationTime, distanceUnit, status, currentSegment, upcomingSegments) yapı olarak kayıt yanıtıyla aynıdır.

Yanıt alanı referansı

LocationStatus

AlanTürAçıklama
progressPercentagesayıYolculuk tamamlanma yüzdesi (0–100)
distanceTraveledsayıRota başından itibaren gidilen mesafe (metre)
remainingDistancesayıVarış noktasına kalan mesafe (metre)
timeElapsedSecondssayıdepartureTime’dan bu yana geçen saniye
estimatedTimeRemainingSecondssayıMevcut hıza göre tahmini kalan seyahat süresi
scheduleDeviationSecondssayıPozitif = zamanın önünde; negatif = zamanın gerisinde
expectedSegmentIndexsayıOrijinal zaman çizelgesine göre sürücünün bulunması gereken segment indeksi
estimatedArrivalTimesayıTahmini varış zamanı (epoch milisaniye), expirationTime ile sınırlandırılmış
expectedSegmentIndex rota zamanlamasını yansıtır, GPS konumunu değil. Sürücü segment 2’deyken zamanlama segment 3’te olmasını bekliyorsa bu alan 3 gösterir. Zamanlama kaymasını tespit etmek için currentSegment.index ile karşılaştırın.

BaseRouteSegment (currentSegment ve upcomingSegments için ortak)

AlanTürAçıklama
indexsayıRotadaki sıfır tabanlı segment konumu
h3AddressstringÇözünürlük 5’teki Uber H3 hücre tanımlayıcısı
etasayıZamanlanmış varış (epoch milisaniye)
etdsayıZamanlanmış ayrılış (epoch milisaniye)
distanceToEntrysayıRota başından hücre girişine kümülatif mesafe (metre)
distanceToExitsayıRota başından hücre çıkışına kümülatif mesafe (metre)
arrivalPointGeoPointRotanın hücreye girdiği koordinat
departurePointGeoPointRotanın hücreden çıktığı koordinat

ForecastWeatherEvents (tüm segmentler)

AlanTürBirimAçıklama
temperaturesayı°C2 m yükseklikte hava sıcaklığı
apparentTemperaturesayı°CHissedilen sıcaklık
humiditysayı%Bağıl nem
cloudCoversayı%Gökyüzü bulut oranı
windSpeedsayıkm/s10 m yükseklikte rüzgar hızı
windGustsayıkm/sMaksimum rüzgar hamlesi
windDirectionsayı°Meteorolojik rüzgar yönü (0 = Kuzey, 90 = Doğu)
snowFallsayıcmKar yağışı birikimi
precipitationsayımmYağış miktarı
precipitationProbabilitysayı%Model yağış olasılığı
visibilitysayımYatay görüş mesafesi

Nowcast özet alanları

currentSegment.weatherEvents nesnesi, nowcast verisi mevcut olduğunda üç ek özet nesne içerir.

LightningSummary — Yıldırım Özeti

Yer tabanlı elektromanyetik sensörlerden türetilen gerçek zamanlı yıldırım aktivitesi.
AlanTürAçıklama
windowMinutessayıToplam gözlem penceresi (örn. 60 dakika)
bucketMinutessayıToplama için kullanılan zaman dilimi çözünürlüğü (örn. 30 dakika)
totalEventCountsayıPencere içinde tüm çakım olayları (bulut içi şimşek + yerden buluta yıldırım)
cgFlashCountsayıYıldırım sayısı — açıkta çalışanlar ve sürücüler için en tehlikeli tür
icPulseCountsayıŞimşek sayısı — fırtına elektriklenme yoğunluğunun göstergesi
lastEventAgeSecsayıHerhangi bir türdeki en son olaydan bu yana geçen saniye
lastFlashAgeSecsayıEn son yıldırımdan bu yana geçen saniye
lastPulseAgeSecsayıEn son şimşekten bu yana geçen saniye
nearestEventDistancesayıHerhangi bir türdeki en yakın olaya metre cinsinden mesafe
nearestFlashDistancesayıEn yakın yıldırıma metre cinsinden mesafe
nearestPulseDistancesayıEn yakın şimşeğe metre cinsinden mesafe
maxPeakCurrentsayıPenceredeki maksimum tepe akımı (kA) — yüksek değerler daha yıkıcı flaşlara işaret eder
avgPeakCurrentsayıOrtalama tepe akımı (kA)
avgSensorCountsayıHer olayı tespit eden ortalama sensör sayısı — yüksek = daha doğru konum
confidenceScoresayı0–1 arası tespit kalitesi güven skoru
confidenceLevelenumLOW / MEDIUM / HIGH
riskIndexsayıBileşik risk skoru (0–100)
riskLevelenumNONE / LOW / MEDIUM / HIGH / EXTREME
trendenumUNKNOWN / DECREASING / STABLE / INCREASING
Mobil uygulama kullanıcısı için anlamı: riskLevel rengine göre bir yıldırım kalkan simgesi gösterin. riskLevel HIGH veya EXTREME ise acil uyarı çıkarın. nearestFlashDistance ve lastFlashAgeSec ile bağlam sağlayın: “45 saniye önce 3,2 km uzakta yıldırım tespit edildi — araçta kalın.”

ThunderstormSummary — Fırtına Özeti

Çok katmanlı radar analizinden türetilen fırtına hücresi takibi.

summary nesnesi

AlanTürAçıklama
stormCountsayıGözlem penceresindeki toplam takip edilen fırtına hücresi sayısı
activeStormssayıŞu anda yıldırım veya şiddetli yağış üreten hücre sayısı
insideAnyThreatBoundarybooleanSürücünün H3 hücresi herhangi bir fırtınanın tehdit poligonuyla kesişiyorsa true
nearestThreatBoundaryDistancesayıEn yakın fırtına tehdit sınırına metre cinsinden mesafe
nearestStormCentroidDistancesayıEn yakın fırtına hücresi merkezine metre cinsinden mesafe
maxSeverityenumTüm takip edilen fırtınalar arasındaki en yüksek şiddet: LOW / NORMAL / HIGH / UNKNOWN
lastEventAgesayıEn son fırtına olayından bu yana geçen saniye
riskScoresayıBileşik risk skoru (0–100)
riskLevelenumNONE / LOW / MEDIUM / HIGH / EXTREME / UNKNOWN
trendenumRISING / STABLE / FALLING / UNKNOWN

activeStorms[] — fırtına başına detay

AlanTürAçıklama
eventIdstringTakip edilen fırtına hücresinin benzersiz tanımlayıcısı
severityenumLOW / NORMAL / HIGH / UNKNOWN
eventStartUtcEpochsayıFırtına hücresinin ilk tespit edildiği zaman (epoch milisaniye)
eventEndUtcEpochsayıSon kaydedilen aktivite zamanı (epoch milisaniye)
eventAgesayıFırtına hücresinin ilk tespitinden bu yana geçen saniye
cell.areasayıFırtına hücresi alanı (km²)
cell.speedsayıFırtına hareket hızı (km/s)
cell.directionsayıFırtına hareket yönü (derece, meteorolojik)
cell.centroidDistancesayıSürücüden fırtına hücresi merkezine mesafe (metre)
cell.directionFromDriverenumPusula yönü: N, NNE, NE, ENE, E, ESE, SE, SSE, S, SSW, SW, WSW, W, WNW, NW, NNW
cell.bearingFromDriversayıSürücüden fırtına merkezine tam açı (derece)
flashRates.inCloudsayıŞimşek hızı (adet/dakika)
flashRates.cloudToGroundsayıYıldırım hızı (adet/dakika)
flashRates.totalsayıToplam çakım hızı (adet/dakika)
flashRates.cloudToGroundRatiosayıYıldırım oranı (0–1); yüksek = daha fazla yıldırım
threat.insideThreatPolygonbooleanSürücü bu fırtınanın tehdit poligonu içindeyse true
threat.distanceToThreatBoundarysayıBu fırtınanın tehdit poligonu sınırına metre cinsinden mesafe
threat.directionFromDriverenumTehdit sınırına pusula yönü
threat.bearingFromDriversayıTehdit sınırına tam açı
threat.approachStateenumAPPROACHING / MOVING_AWAY / STABLE / UNKNOWN
score.stormRiskScoresayıFırtına başına risk skoru (0–100)
score.stormRiskLevelenumNONE / LOW / MEDIUM / HIGH / EXTREME / UNKNOWN
Mobil uygulama kullanıcısı için anlamı: insideAnyThreatBoundary true ise bu anlık bir güvenlik uyarısıdır — sürücü bir fırtınanın tahmin edilen etki bölgesi içindedir. approachState: APPROACHING bayrağı ve nearestThreatBoundaryDistance sürücünün durması mı yoksa devam etmesi mi gerektiğine karar vermesine yardımcı olur. Fırtınanın hangi yönden geldiğini anlamak için directionFromDriver değerini gösterin.

PrecipitationSummary — Yağış Özeti

Mevcut H3 hücresi için radar kaynaklı yağış analizi.
AlanTürAçıklama
windowMinutessayıGözlem penceresi (dakika)
bucketMinutessayıToplama dilimi (dakika)
currentIntensityenumMevcut yoğunluk: DRIZZLE / LIGHT / MODERATE / HEAVY / VERY_HEAVY / EXTREME, yağış yoksa null
isPrecipitatingbooleanHücre üzerinde yağış tespit ediliyorsa true
radarTimeStampsayıKullanılan radar taramasının epoch milisaniyesi
dataAgeSecsayıRadar verisinin yaşı (saniye) — 600 saniyenin altı taze kabul edilir
radarSnapshotCountsayıPencereye dahil edilen radar taraması sayısı
maxIntensityenumPencerede kaydedilen maksimum yoğunluk
trendenumINCREASING / STABLE / DECREASING / UNKNOWN
riskLevelenumNONE / LOW / MEDIUM / HIGH / EXTREME
forecastMaxIntensityenumNWP modelinin yakın gelecek için tahmin ettiği maksimum yoğunluk
expectedEndSecsayıYağışın bitmesi için beklenen süre (saniye) — şu anda yağıyorsa
expectedStartSecsayıYağışın başlaması için beklenen süre (saniye) — şu anda kuru ise
Mobil uygulama kullanıcısı için anlamı: currentIntensity’yi hava rozeti olarak gösterin. expectedStartSec’i önceden uyarı vermek için kullanın — “8 dakika içinde şiddetli yağmur bekleniyor” — böylece sürücüler hazırlık yapabilir (silecekler, hız düşürme). HEAVY veya üzeri proaktif bir anlık bildirim gerektirir.

Rota dışı ve zamanlama dışı durumlar

Rota Dışı (Off-Route)

/weather uç noktası, sürücünün currentLocation konumunun kayıtlı rotanın herhangi bir segmentine 1.000 metre içinde olup olmadığını kontrol eder (dik mesafe). Sürücü bu eşiği aşarsa:
  • Rota oturumu önbellekten anında silinir.
  • error: "Off Route" ile 400 Bad Request döner.
  • routeId artık geçerli değildir.
  • İstemci /register’ı yeni bir rota ile tekrar çağırmalıdır.
{
  "timestamp": "2026-04-14T15:26:45.404520747",
  "status": 400,
  "error": "Off Route",
  "message": "Rota dışına 1.000 metreden daha fazla çıktığınız için mevcut rotanız iptal edildi, lütfen yeni bir rota için kayıt oluşturunuz!",
  "path": "/v1/enroute/weather"
}
UX rehberi: HTTP 400 ve error: "Off Route" kombinasyonunu dinleyin. Kullanıcıya bir iletişim kutusu gösterin: “Rotadan çıktınız. Hava durumu takibini sürdürmek için yeni güzergahınızda navigasyonu başlatın.” Aynı routeId ile /weather’ı yeniden denemeyin.

Zamanlama Dışı (Off-Schedule)

Sürücünün GPS konumu beklenenden önemli ölçüde geride kalıyorsa — özellikle now, sürücünün mevcut segmentinden sonraki segmentin zamanlanmış ETA’sından 10 dakikadan fazla geçtiyse — servis kalan rota segmentlerini sürücünün mevcut konumundan sessizce yeniden hesaplar. Yeniden hesaplama gerçekleştiğinde:
  • Yanıtta segmentsRecalculated: true ayarlanır.
  • warning, kullanıcı dostu bir açıklama içerir.
  • Segment indeksleri, ETA’lar ve mesafeler sürücünün mevcut konumundan sıfırlanır.
  • currentSegment, sürücünün şu anda bulunduğu konumdan başlayan yeni segment 0’ı yansıtır.
  • expirationTime, yeni ETA + 2 saat olarak uzatılır.
segmentsRecalculated true olduğunda, önceki yanıtlardan önbelleğe alınan tüm segment verilerini atın ve yeni yanıttaki tam segment listesini yeniden oluşturun.

Hata referansı

HTTP DurumerrorTetikleyici
400Off RouteSürücü rotadan 1.000 m’den fazla uzaklaştı. Oturum silindi.
400Invalid RouterouteId bulunamadı, oturum süresi doldu veya rota geometrisi geçersiz.
409Duplicate RouteAynı başlangıç → varış çifti için zaten aktif bir oturum var. Mevcut routeId’yi kullanın.
402Payment RequiredAbonelik süresi doldu veya tükendi.
403ForbiddenBu uç nokta için abonelik yok veya sorgu konumu abonelik sınırları dışında.
429Too Many RequestsAbonelik istek limiti aşıldı.
500Data Provider ErrorBir hava durumu veri sağlayıcısına ulaşılamadı. Yanıt, hangi kaynak (Nowcast / Tahmin) ve servisin (Yıldırım, Gök Gürültülü Fırtına, Yağış, Anlık Tahmin, Saatlik Tahmin) başarısız olduğunu içerir.

Yinelenen Rota (409)

{
  "timestamp": "2026-04-15T10:01:00.000000000",
  "status": 409,
  "error": "Duplicate Route",
  "messages": [
    "routeId ile aktif oturum mevcut: 84863286-c5f8-4a2c-9d40-837213e8c38e",
    "Bu oturum sona erme zamanı: 2026-04-15T13:54:00Z",
    "Mevcut routeId ile /weather sorgulamaya devam edin."
  ],
  "path": "/v1/enroute/register"
}

Süresi Dolmuş veya Bulunamadı (400)

{
  "timestamp": "2026-04-14T17:34:14.421986962",
  "status": 400,
  "error": "Invalid Route",
  "message": "Rota bulunamadı veya rotanın süresi doldu.",
  "path": "/v1/enroute/weather"
}

Veri Tedarik Hatası (500)

Altta yatan hava durumu servislerinden (yıldırım, gök gürültülü fırtına, yağış veya tahmin) birine ulaşılamadığında döner. message alanı hangi kaynak ve servisin başarısız olduğunu belirtir; loglama ve kullanıcı dostu geri dönüş mesajı gösterimi için kullanabilirsiniz.
{
  "timestamp": "2026-04-15T10:00:00.000000000",
  "status": 500,
  "error": "Veri Tedarik Hatası",
  "message": "Hava durumu verileri alınırken bir sorun oluştu (Nowcast / Yıldırım). Lütfen daha sonra tekrar deneyin.",
  "path": "/v1/enroute/register"
}

Geliştirici notları

API’deki tüm Instant tipli alanlar 64-bit tam sayı epoch milisaniye olarak serileştirilir — expirationTime, eta, etd, estimatedArrivalTime. Bu değerleri asla saniye olarak yorumlamayın. JavaScript’te doğrudan new Date(value), Java’da Instant.ofEpochMilli(value) kullanın.
API (kullanıcıId, başlangıçH3, varışH3) üzerinde tekilleştirme uygular. Sürücü aktif bir oturum varken aynı rotayı kaydetmeye çalışırsa 409 Conflict döner; messages dizisinde mevcut routeId ve expirationTime bulunur. 409’u fatal hata olarak ele almak yerine messages dizisini ayrıştırarak mevcut routeId’yi çıkarın ve sorgulamaya devam edin.
Oturumlar ETA + 2 saat sonra sona erer. Süresi dolduktan sonra eski routeId ile /weather çağrısı 400 döndürür. Sona erme süresini proaktif olarak takip etmek için her yanıttaki expirationTime ile karşılaştırma yapın.
Sunucu tarafı push yoktur; tüm güncellemeler istemci tarafından başlatılan /weather sorgusu gerektirir. Araç hareket halindeyken önerilen yoklama aralığı: her 30–60 saniyede bir. Araç durakken (örn. trafik durması) 5 dakikaya düşürülebilir.
segmentsRecalculated: true olduğunda segment listesi sürücünün mevcut konumundan yeniden oluşturulmuştur. Segment index değerleri 0’dan yeniden başlar. Önceki segment indekslerini önbelleğe alan UI öğelerinin (ilerleme çubukları, segment listesi kaydırma) yeni yanıt kullanılarak yeniden başlatılması gerekir.
Yanıttaki distanceUnit alanı her zaman "m" değerini alır. distanceToEntry, distanceToExit ve remainingDistance değerlerinin tümü metredir. Km veya mil’e dönüşümü UI katmanında yapın.

Postman ile test

Aşağıdaki Postman görselleştirme scripti, her /register veya /weather isteğinden sonra Visualize sekmesinde okunabilir bir rota özeti render eder — rota durumunu anlamak için ham JSON okumaya gerek kalmaz. Kullanımı:
  1. İsteği Postman’de açın (POST /v1/enroute/register veya POST /v1/enroute/weather).
  2. ScriptsPost-response bölümüne gidin ve aşağıdaki scripti yapıştırın.
  3. İsteği gönderin, ardından Visualize sekmesine geçin.
// 1. Response (Cevap) Verisini Al
const responseData = pm.response.json();

// 2. Request (İstek) Verisini Al
let requestData = {};
if (pm.request.body && pm.request.body.raw) {
    try {
        requestData = JSON.parse(pm.request.body.raw);
    } catch (e) {
        console.log("Request body parse edilemedi: ", e);
    }
}

// HTML/JS/CSS template for visualization
const template = `
<!DOCTYPE html>
<html lang="tr">
<head>
    <meta charset="UTF-8">
    <title>H3 Weather Route</title>
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
    <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
    <script src="https://unpkg.com/h3-js@4.1.0/dist/h3-js.umd.js"></script>
    
    <style>
        body { margin: 0; padding: 0; font-family: Arial, sans-serif; }
        #map { height: 100vh; width: 100vw; }
        
        /* Map Popup Styles */
        .popup-wrapper { min-width: 250px; }
        .popup-content { font-size: 13px; line-height: 1.5; }
        .popup-header { font-weight: bold; font-size: 14px; margin-bottom: 5px; border-bottom: 1px solid #ccc; padding-bottom: 3px; }
        .summary-box { background: #f9f9f9; padding: 5px; margin-top: 5px; border-left: 3px solid #ff9800; }
        .current-badge { background: #d32f2f; color: white; padding: 2px 4px; border-radius: 3px; font-size: 10px; margin-left: 4px; }
        .divider { margin: 6px 0; border: 0; border-top: 1px solid #eee; }
        
        /* Tabs Styles */
        .tab-headers { display: flex; overflow-x: auto; border-bottom: 1px solid #ccc; margin-bottom: 8px; gap: 2px;}
        .tab-headers::-webkit-scrollbar { height: 4px; }
        .tab-headers::-webkit-scrollbar-thumb { background: #ccc; border-radius: 4px;}
        .tab-btn { background: #eee; border: 1px solid #ccc; border-bottom: none; padding: 4px 8px; cursor: pointer; border-radius: 4px 4px 0 0; font-size: 11px; flex-shrink: 0; color: #555;}
        .tab-btn:hover { background: #e0e0e0; }
        .tab-btn.active { background: #fff; font-weight: bold; border-bottom: 1px solid #fff; margin-bottom: -1px; color: #1976d2; }
        .tab-pane { display: none; }
        .tab-pane.active { display: block; }

        /* Weather Icon & Segment Badge Styles */
        .weather-emoji-icon { background: transparent; border: none; }
        .icon-container { position: relative; display: inline-block; text-align: center; }
        .emoji-layer { font-size: 22px; line-height: 1; filter: drop-shadow(0px 0px 3px rgba(255,255,255,0.9)); }
        
        .segment-badge {
            position: absolute;
            top: 22px;
            left: 50%;
            transform: translateX(-50%);
            background: rgba(255, 255, 255, 0.95);
            color: #333;
            font-size: 10px;
            font-weight: bold;
            padding: 1px 5px;
            border-radius: 8px;
            border: 1px solid #ccc;
            box-shadow: 0 1px 3px rgba(0,0,0,0.3);
            white-space: nowrap;
            pointer-events: none;
        }
        .segment-badge.multiple { background: #1976d2; color: white; border-color: #0d47a1; }
        
        /* Current Location Icon Styles (Yellow with shadow) */
        .current-loc-icon {
            background-color: #ffca28;
            border: 3px solid #ffffff;
            border-radius: 50%;
            box-shadow: 0px 3px 8px rgba(0, 0, 0, 0.5);
        }

        /* Legend Box Styles */
        .info.legend { background: white; padding: 10px 14px; border-radius: 8px; box-shadow: 0 4px 15px rgba(0,0,0,0.2); font-size: 13px; line-height: 1.6; min-width: 250px; color: #333; }
        .info.legend h4 { margin: 0 0 8px 0; border-bottom: 2px solid #1976d2; padding-bottom: 4px; color: #1976d2; font-size: 15px; display: flex; align-items: center; gap: 5px;}
        .route-id-text { font-size: 10px; color: #777; word-break: break-all; }
        
        @keyframes blink { 0% { opacity: 1; } 50% { opacity: 0.3; } 100% { opacity: 1; } }
        .active-dot { color: #4caf50; font-size: 12px; animation: blink 2s infinite; }
        
        .deviation-box { background: #f4f4f4; padding: 6px; border-radius: 4px; margin-top: 6px; border-left: 3px solid #ccc; }

        /* Modal Styles */
        .modal-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 10000; }
        .modal-content { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #fff; padding: 20px; border-radius: 8px; min-width: 350px; max-height: 80vh; overflow-y: auto; box-shadow: 0 4px 15px rgba(0,0,0,0.3); }
        .modal-close { float: right; cursor: pointer; font-weight: bold; font-size: 16px; color: #555; border: none; background: #eee; border-radius: 4px; padding: 4px 10px;}
        .modal-close:hover { background: #e0e0e0; color: #d32f2f; }
        .modal-title { margin-top: 0; border-bottom: 1px solid #eee; padding-bottom: 10px; color: #1976d2;}
        
        /* Formatted UI List inside Modals */
        .formatted-list { list-style: none; padding-left: 0; margin: 0; font-size: 13px; }
        .formatted-list li { display: flex; justify-content: space-between; padding: 6px 0; border-bottom: 1px dashed #e0e0e0; align-items: center;}
        .formatted-list li:last-child { border-bottom: none; }
        .formatted-list .lbl { font-weight: bold; color: #444; margin-right: 15px; }
        .formatted-list .val { color: #1976d2; font-weight: 500; text-align: right; }
        .modal-body > ul { padding-left: 0; }
    </style>
</head>
<body>
    <div id="map"></div>
    <div id="detailsModal" class="modal-overlay">
        <div class="modal-content">
            <button class="modal-close" onclick="window.closeModal()">Kapat</button>
            <h3 id="modalTitle" class="modal-title">Detaylar</h3>
            <div id="modalBody" class="modal-body"></div>
        </div>
    </div>

    <script>
        // Response ve Request verilerini al
        const data = {{{payload}}};
        const reqData = {{{reqPayload}}};
        
        const map = L.map('map');
        
        // --- CUSTOM PANES ---
        map.createPane('pointsPane');
        map.getPane('pointsPane').style.zIndex = 600;

        map.createPane('iconsPane');
        map.getPane('iconsPane').style.zIndex = 650;
        map.getPane('iconsPane').style.pointerEvents = 'none';

        L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© OpenStreetMap' }).addTo(map);

        const routeId = data.routeId || reqData.routeId;
        const expTime = data.expirationTime;
        const status = data.status || {};
        const segments = [];
        if (data.currentSegment) segments.push(data.currentSegment);
        if (data.upcomingSegments) segments.push(...data.upcomingSegments);
        
        const allBounds = [];
        window.summaryStore = {};

        // 1. MARK CURRENT LOCATION OR ORIGIN (REQDATA'DAN OKUYOR)
        const activeLoc = reqData.currentLocation || reqData.origin;

        if (activeLoc && activeLoc.lat && activeLoc.lng) {
            const locIcon = L.divIcon({
                className: 'current-loc-icon',
                iconSize: [18, 18],
                iconAnchor: [9, 9]
            });
            
            L.marker([activeLoc.lat, activeLoc.lng], { 
                icon: locIcon,
                zIndexOffset: 2000 
            }).addTo(map).bindTooltip(\`<b>Mevcut Konum</b>\`, { direction: 'top' });
            
            allBounds.push([activeLoc.lat, activeLoc.lng]);
        }

        // Formatters (DEĞİŞTİRİLDİ: Artık ms üzerinden çalışıyor)
        const formatTime = (ts) => ts ? new Date(ts).toLocaleTimeString('tr-TR', { hour: '2-digit', minute: '2-digit' }) : '-';
        const formatDateTime = (ts) => ts ? new Date(ts).toLocaleString('tr-TR', { day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit' }) : '-';
        const formatDist = (m) => m != null ? (m / 1000).toFixed(2) + ' km' : '-';
        
        const formatDuration = (s) => {
            if (s == null) return '-';
            const h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60);
            return h > 0 ? \`\${h} sa \${m} dk\` : \`\${m} dk\`;
        };

        const formatDeviation = (seconds) => {
            if (seconds === undefined || seconds === null) return { status: '-', text: '-', color: '#333' };
            if (seconds === 0) return { status: 'Zamanında', text: '0 dk', color: '#4caf50' };
            
            const isLate = seconds > 0;
            const absSec = Math.abs(seconds);
            const h = Math.floor(absSec / 3600);
            const m = Math.floor((absSec % 3600) / 60);
            
            let timeStr = '';
            if (h > 0) timeStr += \`\${h} sa \`;
            if (m > 0) timeStr += \`\${m} dk\`;
            if (h === 0 && m === 0) timeStr = \`\${Math.floor(absSec)} sn\`;
            
            return {
                status: isLate ? 'Geride (Gecikmeli)' : 'İleride (Erken)',
                text: timeStr.trim(),
                color: isLate ? '#d32f2f' : '#4caf50'
            };
        };

        const getWindInfo = (deg) => {
            if (deg === null || deg === undefined) return { icon: '🌬️', desc: 'Bilinmiyor' };
            const index = Math.round(deg / 45) % 8;
            const dirs = [
                { id: 'Kuzey', icon: '⬇️' }, { id: 'Kuzeydoğu', icon: '↙️' }, { id: 'Doğu', icon: '⬅️' }, { id: 'Güneydoğu', icon: '↖️' },
                { id: 'Güney', icon: '⬆️' }, { id: 'Güneybatı', icon: '↗️' }, { id: 'Batı', icon: '➡️' }, { id: 'Kuzeybatı', icon: '↘️' }
            ];
            return { icon: dirs[index].icon, desc: \`\${dirs[index].id} (\${deg}°)\` };
        };

        // --- YENİ EKLENEN ÇEVİRİ VE FORMATLAMA YAPISI ---
        const translateValue = (val) => {
            if (val === true) return 'Evet';
            if (val === false) return 'Hayır';
            const map = {
              'DRIZZLE': 'Çisenti',
              'LIGHT': 'Hafif',
              'MODERATE': 'Orta',
              'HEAVY': 'Şiddetli',
              'VERY_HEAVY': 'Çok şiddetli',
              'EXTREME': 'Aşırı',

              'INCREASING': 'Artıyor',
              'STABLE': 'Sabit',
              'DECREASING': 'Azalıyor',
              'UNKNOWN': 'Bilinmiyor',

              'NONE': 'Yok',
              'LOW': 'Düşük',
              'MEDIUM': 'Orta',
              'HIGH': 'Yüksek',

              'CONFIDENCE_LOW': 'Düşük',
              'CONFIDENCE_MEDIUM': 'Orta',
              'CONFIDENCE_HIGH': 'Yüksek',

              'NORMAL': 'Normal',

              'RISING': 'Yükseliyor',
              'FALLING': 'Düşüyor',

              'N': 'Kuzey',
              'NNE': 'Kuzey-kuzeydoğu',
              'NE': 'Kuzeydoğu',
              'ENE': 'Doğu-kuzeydoğu',
              'E': 'Doğu',
              'ESE': 'Doğu-güneydoğu',
              'SE': 'Güneydoğu',
              'SSE': 'Güney-güneydoğu',
              'S': 'Güney',
              'SSW': 'Güney-güneybatı',
              'SW': 'Güneybatı',
              'WSW': 'Batı-güneybatı',
              'W': 'Batı',
              'WNW': 'Batı-kuzeybatı',
              'NW': 'Kuzeybatı',
              'NNW': 'Kuzey-kuzeybatı',

              'APPROACHING': 'Yaklaşıyor',
              'MOVING_AWAY': 'Uzaklaşıyor'
            };
            return map[val] !== undefined ? map[val] : val;
        };

        const tzFormat = (ms) => ms ? new Date(ms).toLocaleString('tr-TR', { timeZone: 'Europe/Istanbul', year:'numeric', month:'short', day:'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit' }) : '-';

        const dict = {
            // Precipitation (Yağış)
            currentIntensity: { label: 'Mevcut Şiddet', format: translateValue },
            isPrecipitating: { label: 'Yağış Durumu', format: (v) => v ? 'Yağış Var' : 'Yok' },
            radarTimeStamp: { label: 'Radar Zamanı', format: tzFormat },
            dataAgeSec: { label: 'Veri Yaşı', format: (v) => (v / 1000).toFixed(0) + ' sn' },
            radarSnapshotCount: { label: 'Ölçüm (Snapshot) Sayısı', format: (v) => v + ' Adet' },
            maxIntensity: { label: 'Maksimum Şiddet', format: translateValue },
            trend: { label: 'Şiddet Eğilimi', format: translateValue },
            riskLevel: { label: 'Risk Seviyesi', format: translateValue },
            forecastMaxIntensity: { label: 'Tahmini Maks. Şiddet', format: translateValue },
            expectedEndSec: { label: 'Beklenen Bitiş', format: (v) => v > 0 ? (v / 60000).toFixed(0) + ' dk sonra' : 'Belirsiz / Sona Erdi' },

            // Lightning (Yıldırım)
            totalEventCount: { label: 'Toplam Olay Sayısı', format: (v) => v + ' Adet' },
            cgFlashCount: { label: 'Yıldırım', format: (v) => v + ' Adet' },
            icPulseCount: { label: 'Şimşek', format: (v) => v + ' Adet' },
            confidenceScore: { label: 'Güvenilirlik Skoru', format: (v) => v.toFixed(2) },
            confidenceLevel: { label: 'Güvenilirlik Seviyesi', format: translateValue },
            riskIndex: { label: 'Risk İndeksi', format: (v) => v.toFixed(2) },

            // Thunderstorm (Fırtına)
            stormCount: { label: 'Fırtına Sayısı', format: (v) => v + ' Adet' },
            activeStorms: { label: 'Aktif Fırtınalar', format: (v) => Array.isArray(v) && v.length === 0 ? 'Yok' : v.length + ' Adet' },
            insideAnyThreatBoundary: { label: 'Tehdit Sınırı İçinde mi?', format: translateValue },
            nearestThreatBoundaryDistance: { label: 'En Yakın Tehdit Sınırı', format: (v) => v !== null ? v + ' km' : '-' },
            nearestStormCentroidDistance: { label: 'En Yakın Fırtına Merkezi', format: (v) => v !== null ? v + ' km' : '-' },
            maxSeverity: { label: 'Maksimum Şiddet', format: translateValue },
            lastEventAge: { label: 'Son Olay Yaşı', format: (v) => v !== null ? (v / 1000).toFixed(0) + ' sn' : '-' },
            riskScore: { label: 'Risk Skoru', format: (v) => v.toFixed(2) },

            // Gereksizleri Gizle
            windowMinutes: { skip: true },
            bucketMinutes: { skip: true }
        };

        // UI Interactions
        window.showModal = function(id, title) {
            document.getElementById('modalTitle').innerText = title;
            const dataToRender = window.summaryStore[id] || { error: "Detay verisi bulunamadı." };
            document.getElementById('modalBody').innerHTML = window.buildHtmlList(dataToRender);
            document.getElementById('detailsModal').style.display = 'block';
        };
        window.closeModal = () => document.getElementById('detailsModal').style.display = 'none';

        window.switchTab = function(event, h3Index, targetIdx) {
            const wrapper = event.target.closest('.popup-wrapper');
            wrapper.querySelectorAll('.tab-pane').forEach(p => p.classList.remove('active'));
            wrapper.querySelector('#pane-' + h3Index + '-' + targetIdx).classList.add('active');
            wrapper.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
            event.target.classList.add('active');
        };

        // --- MODAL İÇERİK OLUŞTURUCU ---
        window.buildHtmlList = function(obj) {
            if (Array.isArray(obj)) {
                return obj.length === 0 ? '<span style="color:#999; font-style:italic;">Veri Yok</span>' : 
                       '<ul>' + obj.map(i => '<li>' + window.buildHtmlList(i) + '</li>').join('') + '</ul>';
            }
            if (typeof obj === 'object' && obj !== null) {
                return '<ul class="formatted-list">' + Object.keys(obj).map(k => {
                    const rule = dict[k];
                    if (rule && rule.skip) return ''; // Gizlenecek alanları atla
                    
                    const label = rule ? rule.label : k; // Sözlükte yoksa raw key kullan
                    let val = obj[k];
                    
                    if (rule && rule.format) {
                        val = rule.format(val); // Özel formatlayıcı
                    } else if (typeof val === 'boolean') {
                        val = translateValue(val); // Bool çevirici
                    } else if (typeof val === 'object') {
                        val = window.buildHtmlList(val); // Alt nesne varsa recursive
                    }
                    
                    return '<li><span class="lbl">' + label + ':</span> <span class="val">' + val + '</span></li>';
                }).join('') + '</ul>';
            }
            return obj === null || obj === undefined ? '-' : obj;
        };

        // --- LEGEND ---
        const legend = L.control({ position: 'topright' });
        legend.onAdd = function() {
            const div = L.DomUtil.create('div', 'info legend');
            const devInfo = formatDeviation(status.scheduleDeviationSeconds);
            
            const activeLocLegend = reqData.currentLocation || reqData.origin;
            const currentLocStr = (activeLocLegend && activeLocLegend.lat) 
                ? \`\${activeLocLegend.lat.toFixed(4)}, \${activeLocLegend.lng.toFixed(4)}\` 
                : '-';

            // Etap Sapma Kontrolü İşlemleri
            const currentIndex = (data.currentSegment && data.currentSegment.index) !== undefined ? data.currentSegment.index : '-';
            const expectedIndex = status.expectedSegmentIndex !== undefined ? status.expectedSegmentIndex : '-';
            const estArrivalStr = status.estimatedArrivalTime ? formatDateTime(status.estimatedArrivalTime) : '-';

            let indexHtml = '';
            if (currentIndex !== '-' && expectedIndex !== '-' && currentIndex !== expectedIndex) {
                indexHtml = \`
                    <b>Mevcut Etap:</b> \${currentIndex}<br/>
                    <div style="color: #d32f2f; font-weight: bold; background: #ffebee; padding: 2px 4px; border-radius: 3px; display: inline-block; margin-top:2px;">
                        ⚠️ Beklenen Etap: \${expectedIndex}
                    </div><br/>
                \`;
            } else {
                indexHtml = \`
                    <b>Mevcut Etap:</b> \${currentIndex}<br/>
                    <b>Beklenen Etap:</b> \${expectedIndex}<br/>
                \`;
            }
            
            div.innerHTML = \`
                <h4><span class="active-dot">🟢</span> Aktif Rota Özeti</h4>
                <b>ID:</b> <span class="route-id-text">\${routeId || '-'}</span><br/>
                <b>Geçerlilik Zamanı:</b> \${formatDateTime(expTime)}<br/>
                <b>Mevcut Konum:</b> \${currentLocStr}<br/>
                <hr class="divider" />
                \${indexHtml}
                <b>Tahmini Varış:</b> \${estArrivalStr}<br/>
                <b>İlerleme:</b> <span style="color:\${status.progressPercentage === 100 ? '#4caf50' : '#1976d2'}; font-weight:bold;">%\${status.progressPercentage || 0}</span><br/>
                <b>Katedilen:</b> \${formatDist(status.distanceTraveled)}<br/>
                <b>Kalan:</b> \${formatDist(status.remainingDistance)}<br/>
                <b>Geçen Süre:</b> \${formatDuration(status.timeElapsedSeconds)}<br/>
                <b>Kalan Süre:</b> \${formatDuration(status.estimatedTimeRemainingSeconds)}<br/>
                
                <div class="deviation-box" style="border-left-color: \${devInfo.color};">
                    <b>Sapma Durumu:</b> <span style="color:\${devInfo.color}; font-weight:bold;">\${devInfo.status}</span><br/>
                    <b>Fark:</b> \${devInfo.text}
                </div>
            \`;
            L.DomEvent.disableClickPropagation(div);
            L.DomEvent.disableScrollPropagation(div);
            return div;
        };
        legend.addTo(map);

        // --- WARNING CONTROL (Yeniden Hesaplama Uyarı Divi) ---
        if (data.segmentsRecalculated === true) {
            const warningControl = L.control({ position: 'bottomright' });
            warningControl.onAdd = function() {
                const div = L.DomUtil.create('div', 'info legend');
                div.style.borderLeft = "4px solid #d32f2f";
                div.style.maxWidth = "300px";
                div.innerHTML = \`
                    <h4 style="color: #d32f2f; margin: 0 0 5px 0;">⚠️ Rota Uyarı</h4>
                    <span style="font-size: 12px; font-weight: bold; color: #333;">\${data.warning || "Rota yeniden hesaplanıyor."}</span>
                \`;
                L.DomEvent.disableClickPropagation(div);
                L.DomEvent.disableScrollPropagation(div);
                return div;
            };
            warningControl.addTo(map);
        }

        // Weather Priority
        const getWeatherPriority = (w) => {
            if (!w) return { score: 0, emoji: '☀️' };
            if ((w.lightningSummary && w.lightningSummary.riskLevel !== 'NONE') || 
                (w.thunderstormSummary?.summary?.riskLevel !== 'NONE')) return { score: 5, emoji: '🌩️' };
            if (w.snowFall > 0) return { score: 4, emoji: '❄️' };
            if (w.precipitation > 0) return { score: 3, emoji: '🌧️' };
            if (w.cloudCover > 65) return { score: 2, emoji: '☁️' };
            if (w.cloudCover > 25) return { score: 1, emoji: '⛅' };
            return { score: 0, emoji: '☀️' };
        };

        // Group Segments
        const groupedSegments = {};
        segments.forEach(seg => {
            if (!seg || !seg.h3Address) return;
            if (!groupedSegments[seg.h3Address]) groupedSegments[seg.h3Address] = [];
            groupedSegments[seg.h3Address].push(seg);
            
            if (seg.departurePoint?.lat) {
                L.circleMarker([seg.departurePoint.lat, seg.departurePoint.lng], { 
                    radius: 5, color: '#fff', weight: 1.5, fillColor: '#4caf50', fillOpacity: 1,
                    pane: 'pointsPane' 
                }).addTo(map).bindTooltip(\`Seg \${seg.index} Giriş\`);
            }
            if (seg.arrivalPoint?.lat) {
                L.circleMarker([seg.arrivalPoint.lat, seg.arrivalPoint.lng], { 
                    radius: 5, color: '#fff', weight: 1.5, fillColor: '#ff9800', fillOpacity: 1,
                    pane: 'pointsPane'
                }).addTo(map).bindTooltip(\`Seg \${seg.index} Çıkış\`);
            }
        });

        // Draw Polygons and Icons
        Object.keys(groupedSegments).forEach(h3Index => {
            const group = groupedSegments[h3Index];
            const isGroupCurrent = group.some(seg => seg === data.currentSegment);
            const segIndices = group.map(s => s.index).join(', ');
            const isMultiple = group.length > 1;
            
            const boundary = h3.cellToBoundary(h3Index);
            boundary.forEach(coord => allBounds.push(coord));
            const polygon = L.polygon(boundary, {
                color: isGroupCurrent ? '#d32f2f' : '#1976d2',
                fillColor: isGroupCurrent ? '#ff5252' : '#2196f3',
                fillOpacity: 0.5, weight: 2
            }).addTo(map);

            let maxScore = -1, bestEmoji = '☀️';
            group.forEach(seg => {
                const p = getWeatherPriority(seg.weatherEvents);
                if (p.score > maxScore) { maxScore = p.score; bestEmoji = p.emoji; }
            });

            const weatherIcon = L.divIcon({
                html: \`
                    <div class="icon-container">
                        <div class="emoji-layer">\${bestEmoji}</div>
                        <div class="segment-badge \${isMultiple ? 'multiple' : ''}">\${segIndices}</div>
                    </div>
                \`,
                className: 'weather-emoji-icon', iconSize: [30, 40], iconAnchor: [15, 20],
                pane: 'iconsPane'
            });
            L.marker(h3.cellToLatLng(h3Index), { icon: weatherIcon, interactive: false }).addTo(map);

            let popupHtml = \`<div class="popup-wrapper popup-content"><div class="popup-header">H3: \${h3Index}</div>\`;
            if (isMultiple) {
                popupHtml += \`<div class="tab-headers">\`;
                group.forEach((seg, idx) => {
                    const star = (seg === data.currentSegment) ? '★ ' : '';
                    popupHtml += \`<button class="tab-btn \${idx === 0 ? 'active' : ''}" onclick="window.switchTab(event, '\${h3Index}', \${idx})">\${star}Seg \${seg.index}</button>\`;
                });
                popupHtml += \`</div>\`;
            }

            group.forEach((segment, idx) => {
                const w = segment.weatherEvents || {};
                const wind = getWindInfo(w.windDirection);
                
                popupHtml += \`<div id="pane-\${h3Index}-\${idx}" class="tab-pane \${idx === 0 ? 'active' : ''}">
                    <b>Giriş/Çıkış:</b> \${formatTime(segment.eta)} - \${formatTime(segment.etd)}<br/>
                    <b>Mesafe:</b> \${formatDist(segment.distanceToEntry)} - \${formatDist(segment.distanceToExit)}<br/>
                    <hr class="divider" />
                    <b>Sıcaklık:</b> \${w.temperature}°C (His: \${w.apparentTemperature}°C)<br/>
                    <b>Nem/Bulut:</b> %\${w.humidity} / %\${w.cloudCover}<br/>
                    <b>Rüzgar:</b> \${w.windSpeed} km/sa (Hamle: \${w.windGust || '-'} km/sa) 
                    <span title="Yön: \${wind.desc}" style="cursor:help; border-bottom:1px dashed #999; margin-left:4px;">\${wind.icon}</span><br/>\`;

                let precipText = \`<b>Yağmur:</b> \${w.precipitation || 0} mm\`;
                if (w.precipitationProbability !== undefined) {
                    precipText += \` (Olasılık: %\${w.precipitationProbability})\`;
                }
                precipText += \` | <b>Kar:</b> \${w.snowFall || 0} mm\`;
                
                popupHtml += precipText + \`<br/>\`;
                
                if (w.visibility !== undefined) {
                    popupHtml += \`<b>Görüş:</b> \${w.visibility} m<br/>\`;
                }
                
                if (w.lightningSummary || w.thunderstormSummary || w.precipitationSummary) {
                    popupHtml += \`<div class="summary-box"><b>Özet Riskler:</b><br/>\`;
                    [
                        {propName: 'lightningSummary', data: w.lightningSummary, label: 'Yıldırım'},
                        {propName: 'thunderstormSummary', data: w.thunderstormSummary?.summary, label: 'Fırtına'},
                        {propName: 'precipitationSummary', data: w.precipitationSummary, label: 'Yağış'}
                    ].forEach(item => {
                        if (item.data) {
                            const r = item.data.riskLevel || 'NONE';
                            if (r !== 'NONE') {
                                const id = \`\${item.propName}_\${segment.index}\`;
                                window.summaryStore[id] = segment.weatherEvents[item.propName];
                                popupHtml += \`\${item.label}: <a href="javascript:void(0)" onclick="window.showModal('\${id}', '\${item.label} Detayları')">\${translateValue(r)}</a><br/>\`;
                            } else { 
                                popupHtml += \`\${item.label}: \${translateValue(r)}<br/>\`; 
                            }
                        }
                    });
                    popupHtml += \`</div>\`;
                }
                popupHtml += \`</div>\`;
            });
            popupHtml += \`</div>\`;
            polygon.bindPopup(popupHtml);
        });

        if (allBounds.length > 0) map.fitBounds(allBounds);
    </script>
</body>
</html>
`;

// Hem response hem de request verisini Visualizer'a aktar
pm.visualizer.set(template, { 
    payload: JSON.stringify(responseData),
    reqPayload: JSON.stringify(requestData)
});