> ## Documentation Index
> Fetch the complete documentation index at: https://api-docs.iklim.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Hizmet Detayları

> Sürücüler için anlık hava durumu izlemeli güzergah takibi. Rotayı bir kez kaydedin, yolculuk boyunca her segment için gerçek zamanlı ve tahminsel hava verilerini alın.

<CardGroup cols={3}>
  <Card title="⚡ Gerçek zamanlı nowcast" icon="bolt">
    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.
  </Card>

  <Card title="🔭 İleriye dönük tahmin" icon="cloud-sun">
    İlerleyen segmentler saatler önceden koşulları planlamak için saatlik NWP tahmin verisiyle zenginleştirilir.
  </Card>

  <Card title="🗺️ Akıllı segmentasyon" icon="hexagon">
    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.
  </Card>
</CardGroup>

***

## 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ç Nokta                    | Amaç                                                                                                                              |
| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `POST /v1/enroute/register` | Rota oturumu oluşturur. `routeId` ve ilk hava durumu görünümünü döndürür.                                                         |
| `POST /v1/enroute/weather`  | Sü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. |

<Note>
  İsteklerdeki ve yanıtlardaki tüm zaman damgaları **epoch milisaniyedir** (64-bit tam sayı). Asla ISO string veya ondalık saniye göndermeyiniz.
</Note>

***

## 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)

<Tip>
  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.
</Tip>

**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:

| Nesne                  | Kaynak                        | Ne söyler                                                          |
| ---------------------- | ----------------------------- | ------------------------------------------------------------------ |
| `lightningSummary`     | Yıldırım tespit sensörleri    | Flaş sayıları, tepe akımı, risk endeksi, yaklaşım trendi           |
| `thunderstormSummary`  | Radar fırtına takip hücreleri | Aktif fırtına listesi, tehdit sınırı yakınlığı, yön, şiddet        |
| `precipitationSummary` | Radar reflektivitesi          | Mevcut 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.

<Note>
  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.
</Note>

***

## Rota kaydı

### İstek

```json theme={null}
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
  }
}
```

| Alan                         | Tür         | Zorunlu              | Açıklama                                                   |
| ---------------------------- | ----------- | -------------------- | ---------------------------------------------------------- |
| `origin`                     | GeoPoint    | ✅                    | Başlangıç koordinatı `{ lat, lng }`                        |
| `destination`                | GeoPoint    | ✅                    | Bitiş koordinatı `{ lat, lng }`                            |
| `departureTime`              | sayı        | ✅                    | Planlanan kalkış zamanı (epoch milisaniye)                 |
| `distanceMeters`             | sayı        | ✅                    | Toplam rota uzunluğu (metre)                               |
| `durationSeconds`            | sayı        | ✅                    | Tahmini seyahat süresi (saniye)                            |
| `geometry.encodedPolyline`   | string      | ✅ (veya coordinates) | Google kodlamalı polyline                                  |
| `geometry.polylinePrecision` | sayı        | —                    | Hassasiyet katsayısı (5 = standart, 6 = yüksek hassasiyet) |
| `geometry.coordinates`       | GeoPoint\[] | ✅ (veya polyline)    | Polyline yerine açık koordinat listesi                     |
| `waypoints`                  | GeoPoint\[] | —                    | Opsiyonel ara durak noktaları (bilgi amaçlı)               |

<Warning>
  `encodedPolyline` (ile `polylinePrecision`) veya `coordinates` alanlarından biri mutlaka sağlanmalıdır. İkisi de gönderilmezse 400 hatası döner.
</Warning>

### Yanıt

```json theme={null}
{
  "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
      }
    }
  ]
}
```

<Tip>
  `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-dışı-ve-zamanlama-dışı-durumlar)).
</Tip>

***

## Rota hava durumu sorgulama

### İstek

```json theme={null}
POST /v1/enroute/weather
Content-Type: application/json
Authorization: Bearer <token>

{
  "routeId": "84863286-c5f8-4a2c-9d40-837213e8c38e",
  "currentLocation": { "lat": 40.9871, "lng": 39.6543 }
}
```

| Alan              | Tür         | Zorunlu | Açıklama                                   |
| ----------------- | ----------- | ------- | ------------------------------------------ |
| `routeId`         | UUID string | ✅       | `/register`'dan dönen oturum tanımlayıcısı |
| `currentLocation` | GeoPoint    | ✅       | Sürücünün anlık GPS koordinatı             |

### Yanıt

`GetRouteWeatherResponse`, `RouteRegistrationResponse`'ı genişletir ve şunları ekler:

| Alan                   | Tür     | Açıklama                                                                   |
| ---------------------- | ------- | -------------------------------------------------------------------------- |
| `segmentsRecalculated` | boolean | Zamanlama dışı kalma nedeniyle segmentler yeniden hesaplandıysa `true`     |
| `warning`              | string  | Yeniden 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`

| Alan                            | Tür  | Açıklama                                                                       |
| ------------------------------- | ---- | ------------------------------------------------------------------------------ |
| `progressPercentage`            | sayı | Yolculuk tamamlanma yüzdesi (0–100)                                            |
| `distanceTraveled`              | sayı | Rota başından itibaren gidilen mesafe (metre)                                  |
| `remainingDistance`             | sayı | Varış noktasına kalan mesafe (metre)                                           |
| `timeElapsedSeconds`            | sayı | `departureTime`'dan bu yana geçen saniye                                       |
| `estimatedTimeRemainingSeconds` | sayı | Mevcut hıza göre tahmini kalan seyahat süresi                                  |
| `scheduleDeviationSeconds`      | sayı | Pozitif = zamanın önünde; negatif = zamanın gerisinde                          |
| `expectedSegmentIndex`          | sayı | Orijinal zaman çizelgesine göre sürücünün *bulunması gereken* segment indeksi  |
| `estimatedArrivalTime`          | sayı | Tahmini varış zamanı (epoch milisaniye), `expirationTime` ile sınırlandırılmış |

<Note>
  `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.
</Note>

### `BaseRouteSegment` (`currentSegment` ve `upcomingSegments` için ortak)

| Alan              | Tür      | Açıklama                                              |
| ----------------- | -------- | ----------------------------------------------------- |
| `index`           | sayı     | Rotadaki sıfır tabanlı segment konumu                 |
| `h3Address`       | string   | Çözünürlük 5'teki Uber H3 hücre tanımlayıcısı         |
| `eta`             | sayı     | Zamanlanmış varış (epoch milisaniye)                  |
| `etd`             | sayı     | Zamanlanmış ayrılış (epoch milisaniye)                |
| `distanceToEntry` | sayı     | Rota başından hücre girişine kümülatif mesafe (metre) |
| `distanceToExit`  | sayı     | Rota başından hücre çıkışına kümülatif mesafe (metre) |
| `arrivalPoint`    | GeoPoint | Rotanın hücreye girdiği koordinat                     |
| `departurePoint`  | GeoPoint | Rotanın hücreden çıktığı koordinat                    |

### `ForecastWeatherEvents` (tüm segmentler)

| Alan                       | Tür  | Birim | Açıklama                                        |
| -------------------------- | ---- | ----- | ----------------------------------------------- |
| `temperature`              | sayı | °C    | 2 m yükseklikte hava sıcaklığı                  |
| `apparentTemperature`      | sayı | °C    | Hissedilen sıcaklık                             |
| `humidity`                 | sayı | %     | Bağıl nem                                       |
| `cloudCover`               | sayı | %     | Gökyüzü bulut oranı                             |
| `windSpeed`                | sayı | km/s  | 10 m yükseklikte rüzgar hızı                    |
| `windGust`                 | sayı | km/s  | Maksimum rüzgar hamlesi                         |
| `windDirection`            | sayı | °     | Meteorolojik rüzgar yönü (0 = Kuzey, 90 = Doğu) |
| `snowFall`                 | sayı | cm    | Kar yağışı birikimi                             |
| `precipitation`            | sayı | mm    | Yağış miktarı                                   |
| `precipitationProbability` | sayı | %     | Model yağış olasılığı                           |
| `visibility`               | sayı | m     | Yatay 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.

| Alan                   | Tür  | Açıklama                                                                                |
| ---------------------- | ---- | --------------------------------------------------------------------------------------- |
| `windowMinutes`        | sayı | Toplam gözlem penceresi (örn. 60 dakika)                                                |
| `bucketMinutes`        | sayı | Toplama için kullanılan zaman dilimi çözünürlüğü (örn. 30 dakika)                       |
| `totalEventCount`      | sayı | Pencere içinde tüm çakım olayları (bulut içi şimşek + yerden buluta yıldırım)           |
| `cgFlashCount`         | sayı | Yıldırım sayısı — açıkta çalışanlar ve sürücüler için en tehlikeli tür                  |
| `icPulseCount`         | sayı | Şimşek sayısı — fırtına elektriklenme yoğunluğunun göstergesi                           |
| `lastEventAgeSec`      | sayı | Herhangi bir türdeki en son olaydan bu yana geçen saniye                                |
| `lastFlashAgeSec`      | sayı | En son yıldırımdan bu yana geçen saniye                                                 |
| `lastPulseAgeSec`      | sayı | En son şimşekten bu yana geçen saniye                                                   |
| `nearestEventDistance` | sayı | Herhangi bir türdeki en yakın olaya metre cinsinden mesafe                              |
| `nearestFlashDistance` | sayı | En yakın yıldırıma metre cinsinden mesafe                                               |
| `nearestPulseDistance` | sayı | En yakın şimşeğe metre cinsinden mesafe                                                 |
| `maxPeakCurrent`       | sayı | Penceredeki maksimum tepe akımı (kA) — yüksek değerler daha yıkıcı flaşlara işaret eder |
| `avgPeakCurrent`       | sayı | Ortalama tepe akımı (kA)                                                                |
| `avgSensorCount`       | sayı | Her olayı tespit eden ortalama sensör sayısı — yüksek = daha doğru konum                |
| `confidenceScore`      | sayı | 0–1 arası tespit kalitesi güven skoru                                                   |
| `confidenceLevel`      | enum | `LOW` / `MEDIUM` / `HIGH`                                                               |
| `riskIndex`            | sayı | Bileşik risk skoru (0–100)                                                              |
| `riskLevel`            | enum | `NONE` / `LOW` / `MEDIUM` / `HIGH` / `EXTREME`                                          |
| `trend`                | enum | `UNKNOWN` / `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

| Alan                            | Tür     | Açıklama                                                                                       |
| ------------------------------- | ------- | ---------------------------------------------------------------------------------------------- |
| `stormCount`                    | sayı    | Gözlem penceresindeki toplam takip edilen fırtına hücresi sayısı                               |
| `activeStorms`                  | sayı    | Şu anda yıldırım veya şiddetli yağış üreten hücre sayısı                                       |
| `insideAnyThreatBoundary`       | boolean | Sürücünün H3 hücresi herhangi bir fırtınanın tehdit poligonuyla kesişiyorsa `true`             |
| `nearestThreatBoundaryDistance` | sayı    | En yakın fırtına tehdit sınırına metre cinsinden mesafe                                        |
| `nearestStormCentroidDistance`  | sayı    | En yakın fırtına hücresi merkezine metre cinsinden mesafe                                      |
| `maxSeverity`                   | enum    | Tüm takip edilen fırtınalar arasındaki en yüksek şiddet: `LOW` / `NORMAL` / `HIGH` / `UNKNOWN` |
| `lastEventAge`                  | sayı    | En son fırtına olayından bu yana geçen saniye                                                  |
| `riskScore`                     | sayı    | Bileşik risk skoru (0–100)                                                                     |
| `riskLevel`                     | enum    | `NONE` / `LOW` / `MEDIUM` / `HIGH` / `EXTREME` / `UNKNOWN`                                     |
| `trend`                         | enum    | `RISING` / `STABLE` / `FALLING` / `UNKNOWN`                                                    |

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

| Alan                              | Tür     | Açıklama                                                                                                        |
| --------------------------------- | ------- | --------------------------------------------------------------------------------------------------------------- |
| `eventId`                         | string  | Takip edilen fırtına hücresinin benzersiz tanımlayıcısı                                                         |
| `severity`                        | enum    | `LOW` / `NORMAL` / `HIGH` / `UNKNOWN`                                                                           |
| `eventStartUtcEpoch`              | sayı    | Fırtına hücresinin ilk tespit edildiği zaman (epoch milisaniye)                                                 |
| `eventEndUtcEpoch`                | sayı    | Son kaydedilen aktivite zamanı (epoch milisaniye)                                                               |
| `eventAge`                        | sayı    | Fırtına hücresinin ilk tespitinden bu yana geçen saniye                                                         |
| `cell.area`                       | sayı    | Fırtına hücresi alanı (km²)                                                                                     |
| `cell.speed`                      | sayı    | Fırtına hareket hızı (km/s)                                                                                     |
| `cell.direction`                  | sayı    | Fırtına hareket yönü (derece, meteorolojik)                                                                     |
| `cell.centroidDistance`           | sayı    | Sürücüden fırtına hücresi merkezine mesafe (metre)                                                              |
| `cell.directionFromDriver`        | enum    | Pusula yönü: `N`, `NNE`, `NE`, `ENE`, `E`, `ESE`, `SE`, `SSE`, `S`, `SSW`, `SW`, `WSW`, `W`, `WNW`, `NW`, `NNW` |
| `cell.bearingFromDriver`          | sayı    | Sürücüden fırtına merkezine tam açı (derece)                                                                    |
| `flashRates.inCloud`              | sayı    | Şimşek hızı (adet/dakika)                                                                                       |
| `flashRates.cloudToGround`        | sayı    | Yıldırım hızı (adet/dakika)                                                                                     |
| `flashRates.total`                | sayı    | Toplam çakım hızı (adet/dakika)                                                                                 |
| `flashRates.cloudToGroundRatio`   | sayı    | Yıldırım oranı (0–1); yüksek = daha fazla yıldırım                                                              |
| `threat.insideThreatPolygon`      | boolean | Sürücü bu fırtınanın tehdit poligonu içindeyse `true`                                                           |
| `threat.distanceToThreatBoundary` | sayı    | Bu fırtınanın tehdit poligonu sınırına metre cinsinden mesafe                                                   |
| `threat.directionFromDriver`      | enum    | Tehdit sınırına pusula yönü                                                                                     |
| `threat.bearingFromDriver`        | sayı    | Tehdit sınırına tam açı                                                                                         |
| `threat.approachState`            | enum    | `APPROACHING` / `MOVING_AWAY` / `STABLE` / `UNKNOWN`                                                            |
| `score.stormRiskScore`            | sayı    | Fırtına başına risk skoru (0–100)                                                                               |
| `score.stormRiskLevel`            | enum    | `NONE` / `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.

| Alan                   | Tür     | Açıklama                                                                                                   |
| ---------------------- | ------- | ---------------------------------------------------------------------------------------------------------- |
| `windowMinutes`        | sayı    | Gözlem penceresi (dakika)                                                                                  |
| `bucketMinutes`        | sayı    | Toplama dilimi (dakika)                                                                                    |
| `currentIntensity`     | enum    | Mevcut yoğunluk: `DRIZZLE` / `LIGHT` / `MODERATE` / `HEAVY` / `VERY_HEAVY` / `EXTREME`, yağış yoksa `null` |
| `isPrecipitating`      | boolean | Hücre üzerinde yağış tespit ediliyorsa `true`                                                              |
| `radarTimeStamp`       | sayı    | Kullanılan radar taramasının epoch milisaniyesi                                                            |
| `dataAgeSec`           | sayı    | Radar verisinin yaşı (saniye) — 600 saniyenin altı taze kabul edilir                                       |
| `radarSnapshotCount`   | sayı    | Pencereye dahil edilen radar taraması sayısı                                                               |
| `maxIntensity`         | enum    | Pencerede kaydedilen maksimum yoğunluk                                                                     |
| `trend`                | enum    | `INCREASING` / `STABLE` / `DECREASING` / `UNKNOWN`                                                         |
| `riskLevel`            | enum    | `NONE` / `LOW` / `MEDIUM` / `HIGH` / `EXTREME`                                                             |
| `forecastMaxIntensity` | enum    | NWP modelinin yakın gelecek için tahmin ettiği maksimum yoğunluk                                           |
| `expectedEndSec`       | sayı    | Yağışın bitmesi için beklenen süre (saniye) — şu anda yağıyorsa                                            |
| `expectedStartSec`     | sayı    | 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.**

```json theme={null}
{
  "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.

<Tip>
  `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.
</Tip>

***

## Hata referansı

| HTTP Durum | `error`               | Tetikleyici                                                                                                                                                                                          |
| ---------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400        | `Off Route`           | Sürücü rotadan 1.000 m'den fazla uzaklaştı. Oturum silindi.                                                                                                                                          |
| 400        | `Invalid Route`       | `routeId` bulunamadı, oturum süresi doldu veya rota geometrisi geçersiz.                                                                                                                             |
| 409        | `Duplicate Route`     | Aynı başlangıç → varış çifti için zaten aktif bir oturum var. Mevcut `routeId`'yi kullanın.                                                                                                          |
| 402        | `Payment Required`    | Abonelik süresi doldu veya tükendi.                                                                                                                                                                  |
| 403        | `Forbidden`           | Bu uç nokta için abonelik yok veya sorgu konumu abonelik sınırları dışında.                                                                                                                          |
| 429        | `Too Many Requests`   | Abonelik istek limiti aşıldı.                                                                                                                                                                        |
| 500        | `Data Provider Error` | Bir 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)

```json theme={null}
{
  "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)

```json theme={null}
{
  "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.

<Tabs>
  <Tab title="Nowcast hatası">
    ```json theme={null}
    {
      "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"
    }
    ```
  </Tab>

  <Tab title="Tahmin hatası">
    ```json theme={null}
    {
      "timestamp": "2026-04-15T10:00:00.000000000",
      "status": 500,
      "error": "Veri Tedarik Hatası",
      "message": "Hava durumu verileri alınırken bir sorun oluştu (Tahmin / Saatlik Tahmin). Lütfen daha sonra tekrar deneyin.",
      "path": "/v1/enroute/register"
    }
    ```
  </Tab>
</Tabs>

***

## Geliştirici notları

<AccordionGroup>
  <Accordion title="Tüm zaman damgaları epoch milisaniyedir" icon="clock">
    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.
  </Accordion>

  <Accordion title="Başlangıç–varış çifti başına bir aktif oturum" icon="copy">
    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.
  </Accordion>

  <Accordion title="Oturum sona erme" icon="timer">
    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.
  </Accordion>

  <Accordion title="Yoklama sıklığı" icon="rotate">
    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.
  </Accordion>

  <Accordion title="segmentsRecalculated işleme" icon="arrows-rotate">
    `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.
  </Accordion>

  <Accordion title="distanceUnit her zaman metredir" icon="ruler">
    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.
  </Accordion>
</AccordionGroup>

***

## 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. **Scripts** → **Post-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.

```javascript theme={null}
// 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; 
            -webkit-font-smoothing: antialiased; 
            -moz-osx-font-smoothing: grayscale; 
            text-rendering: optimizeLegibility; 
        }
        #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; 
            align-items: center; 
			justify-content: center; 
        }
        .modal-content { 
            background: #fff; 
			padding: 20px; 
			border-radius: 8px; 
            min-width: 380px; 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
        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}°)\` };
        };

        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') : (v + ' 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 / 1000).toFixed(2) + ' km' : '-' },
            nearestStormCentroidDistance: { label: 'En Yakın Fırtına Merkezi', format: (v) => v !== null ? (v / 1000).toFixed(2) + ' 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) },
            
            // Fırtına Listesi Formatlayıcı
            activeStormsList: {
                hideLabel: true,
                format: (storms) => {
                    if (!Array.isArray(storms) || storms.length === 0) return '';
                    let html = '<div class="storm-tabs-container" style="margin-top: 15px; border: 1px solid #ccc; border-radius: 6px; overflow: hidden; width: 100%;">';
                    html += '<div class="tab-headers" style="background: #eee; padding: 4px 4px 0 4px; margin: 0; border-bottom: 1px solid #ccc;">';
                    storms.forEach((storm, idx) => {
                        html += \`<button class="tab-btn \${idx === 0 ? 'active' : ''}" onclick="window.switchStormTab(event, '\${storm.eventId}')" style="font-size: 11px; padding: 6px 10px;">\${storm.eventId}</button>\`;
                    });
                    html += '</div>';
                    
                    storms.forEach((storm, idx) => {
                        const details = {
                           "---Genel Bilgiler---": "",
                           "Şiddet": storm.severity ? translateValue(storm.severity) : '-',
                           "Başlangıç": tzFormat(storm.eventStartUtcEpoch),
                           "Bitiş": tzFormat(storm.eventEndUtcEpoch),
                           "Olay Yaşı": storm.eventAge !== undefined ? (storm.eventAge / 1000).toFixed(0) + ' sn' : '-',
                           
                           "---Hücre (Cell) Detayları---": "",
                           "Hücre Alanı": storm.cell?.area ? storm.cell.area.toFixed(2) + ' km²' : '-',
                           "Hız": storm.cell?.speed ? storm.cell.speed + ' km/sa' : '-',
                           "Yön (Derece)": storm.cell?.direction !== undefined ? storm.cell.direction + '°' : '-',
                           "Merkez Mesafesi": storm.cell?.centroidDistance ? (storm.cell.centroidDistance / 1000).toFixed(2) + ' km' : '-',
                           "Sürücüye Göre Yön": storm.cell?.directionFromDriver ? translateValue(storm.cell.directionFromDriver) : '-',
                           "Sürücüye Göre Açı": storm.cell?.bearingFromDriver !== undefined ? storm.cell.bearingFromDriver + '°' : '-',
                           
                           "---Yıldırım (Flash) Detayları---": "",
                           "Bulut İçi Şimşek": storm.flashRates?.inCloud !== undefined ? storm.flashRates.inCloud : '-',
                           "Yere Düşen Yıldırım": storm.flashRates?.cloudToGround !== undefined ? storm.flashRates.cloudToGround : '-',
                           "Toplam Şimşek/Yıldırım": storm.flashRates?.total !== undefined ? storm.flashRates.total : '-',
                           "Yıldırım Oranı": storm.flashRates?.cloudToGroundRatio !== undefined ? storm.flashRates.cloudToGroundRatio.toFixed(2) : '-',
                           
                           "---Tehdit (Threat) Detayları---": "",
                           "Poligon İçinde mi?": storm.threat?.insideThreatPolygon !== undefined ? translateValue(storm.threat.insideThreatPolygon) : '-',
                           "Sınıra Mesafe": storm.threat?.distanceToThreatBoundary !== undefined ? (storm.threat.distanceToThreatBoundary / 1000).toFixed(2) + ' km' : '-',
                           "Tehdit Yönü": storm.threat?.directionFromDriver ? translateValue(storm.threat.directionFromDriver) : '-',
                           "Tehdit Açısı": storm.threat?.bearingFromDriver !== undefined ? storm.threat.bearingFromDriver + '°' : '-',
                           "Yaklaşma Durumu": storm.threat?.approachState ? translateValue(storm.threat.approachState) : '-',
                           
                           "---Risk Skorlaması---": "",
                           "Risk Skoru": storm.score?.stormRiskScore !== undefined ? storm.score.stormRiskScore : '-',
                           "Risk Seviyesi": storm.score?.stormRiskLevel ? translateValue(storm.score.stormRiskLevel) : '-'
                        };
                        
                        html += \`<div id="storm-pane-\${storm.eventId}" class="tab-pane storm-pane \${idx === 0 ? 'active' : ''}" style="padding: 10px; background: #fff;">\`;
                        html += '<ul class="formatted-list" style="margin:0;">' + Object.keys(details).map(k => {
                            // Alt başlıklar (kategoriler) için özel render kontrolü
                            if (k.startsWith("---")) {
                                const sectionName = k.replace(/---/g, "");
                                return \`<li style="background: #f0f4f8; padding: 4px 8px; font-weight: bold; color: #1976d2; font-size: 11px; margin-top: 8px; border-radius: 4px; border: 1px solid #d9e2ec; justify-content: flex-start;">\${sectionName}</li>\`;
                            }
                            return \`<li><span class="lbl" style="color:#555;">\${k}:</span> <span class="val">\${details[k]}</span></li>\`;
                        }).join('') + '</ul>';
                        html += \`</div>\`;
                    });
                    html += '</div>';
                    return html;
                }
            },

            // 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 = 'flex'; // <--- BLOK YERİNE FLEX YAPILDI
        };

        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');
        };

        window.switchStormTab = function(event, eventId) {
            const container = event.target.closest('.storm-tabs-container');
            container.querySelectorAll('.storm-pane').forEach(p => p.classList.remove('active'));
            container.querySelector('#storm-pane-' + eventId).classList.add('active');
            container.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
                    }
                    
                    // Eğer hideLabel kullanılmışsa "key:" kısmını gizle ve bloğu %100 genişliğe yay
                    if (rule && rule.hideLabel) {
                        return \`<li style="display:block; padding: 0; border: none;">\${val}</li>\`;
                    }
                    
                    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}\`;
                                
                                if (item.propName === 'thunderstormSummary') {
                                    window.summaryStore[id] = {
                                        ...segment.weatherEvents.thunderstormSummary.summary,
                                        activeStormsList: segment.weatherEvents.thunderstormSummary.activeStorms
                                    };
                                } else {
                                    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)
});
```
