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

# Service Details

> Weather-aware route monitoring for drivers. Register a route once, then poll for real-time and forecast weather conditions segment by segment as the journey unfolds.

<CardGroup cols={3}>
  <Card title="⚡ Real-time nowcast" icon="bolt">
    The current segment always carries live radar-derived lightning, thunderstorm, and precipitation data — not model estimates.
  </Card>

  <Card title="🔭 Forecast ahead" icon="cloud-sun">
    Upcoming segments are enriched with hourly NWP forecast weather so drivers can plan for conditions hours away.
  </Card>

  <Card title="🗺️ Smart segmentation" icon="hexagon">
    The route is split into Uber H3 hexagonal cells. Each cell gets its own ETA and independent weather query, keeping data fresh and spatially accurate.
  </Card>
</CardGroup>

***

## What is the Enroute API?

The Enroute API turns a planned route into a live weather-monitoring session. You provide the route geometry, departure time, and duration — the API splits the path into geographic segments, attaches scheduled arrival times to each one, and returns weather data for every segment on every subsequent poll.

**Two endpoints drive the entire lifecycle:**

| Endpoint                    | Purpose                                                                                                                        |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `POST /v1/enroute/register` | Create a route session. Returns a `routeId` and the initial weather picture.                                                   |
| `POST /v1/enroute/weather`  | Poll an active session with the driver's current location. Returns updated weather, position status, and any schedule changes. |

<Note>
  All timestamps in requests and responses are **epoch milliseconds** (64-bit integers). Never send ISO strings or fractional seconds.
</Note>

***

## How segmentation works

When you register a route, the service decodes the route geometry (encoded polyline or coordinate list) and maps every point to an **Uber H3 hexagonal cell** at resolution 5. Consecutive points falling in the same cell are merged, yielding an ordered list of unique cells — the **segments**.

Each segment receives:

* **`eta`** — scheduled arrival time, interpolated from `departureTime + (distanceToEntry / totalDistance) × durationSeconds`
* **`etd`** — scheduled departure time (same interpolation using `distanceToExit`)
* **`distanceToEntry` / `distanceToExit`** — cumulative metres from route start
* **`arrivalPoint` / `departurePoint`** — the geographic coordinates where the route enters and exits the cell
* **`h3Address`** — the H3 cell identifier (used for weather queries)

<Tip>
  H3 resolution 5 cells cover roughly **252 km²** each, comparable to a medium-sized city district. A typical 90-minute highway route passes through 7–10 segments.
</Tip>

**Why segment?**
Weather is spatially heterogeneous. Rain may be falling at your destination while skies are clear 80 km back. Segmentation lets the API query the right weather model cell for each portion of the journey and schedule those queries to the driver's actual ETA — not all at once.

***

## Nowcast vs Forecast

The response contains two structurally different weather payloads depending on the segment's time horizon.

### `currentSegment` — Nowcast + Forecast

The segment the driver is currently in (or the first segment, at registration time) receives **nowcast** data: real-time observations derived from live radar and sensor networks. This is the richest, most accurate weather picture available.

Nowcast includes three dedicated summary objects:

| Object                 | Source                      | What it tells you                                               |
| ---------------------- | --------------------------- | --------------------------------------------------------------- |
| `lightningSummary`     | Lightning detection sensors | Flash counts, peak current, risk index, approach trend          |
| `thunderstormSummary`  | Radar storm tracking cells  | Active storm list, threat boundary proximity, bearing, severity |
| `precipitationSummary` | Radar reflectivity          | Current intensity, trend, expected start/end, forecast max      |

**Why can upcoming segments not use nowcast?**
Each of the three nowcast services carries a different but complementary constraint:

* **Lightning:** Sensor networks record only events that have already occurred. They produce no forward-looking output whatsoever. A lightning nowcast for a cell the driver will reach two hours from now is physically impossible.
* **Thunderstorm:** Radar-based cell tracking observes the current position, speed, and direction of active storm cells. It generates no forecast projection; a meaningful threat assessment for a segment hours ahead cannot be derived from it.
* **Precipitation:** Alongside live radar observations, a very short-range NWP projection may also be available; this is what feeds the `expectedStartSec` / `expectedEndSec` fields in `precipitationSummary`. However, this horizon only extends slightly beyond the current observation window and does not represent conditions for a segment hours ahead.

In short: the value of all three services lies in answering **"right here, right now."** Once that question becomes meaningless for a future segment, multi-hour NWP forecast models take over.

### `upcomingSegments` — Forecast only

All segments after the current one carry NWP (Numerical Weather Prediction) hourly forecast data: temperature, apparent temperature, humidity, cloud cover, wind speed/gust/direction, precipitation, precipitation probability, snowfall, and visibility.

<Note>
  At registration time, if `departureTime` is within 60 minutes of `now`, the current segment also fetches nowcast data. If the departure is further in the future, only forecast data is returned for the first segment as well.
</Note>

***

## Registering a route

### Request

```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
  }
}
```

| Field                        | Type        | Required           | Description                                         |
| ---------------------------- | ----------- | ------------------ | --------------------------------------------------- |
| `origin`                     | GeoPoint    | ✅                  | Starting coordinate `{ lat, lng }`                  |
| `destination`                | GeoPoint    | ✅                  | Ending coordinate `{ lat, lng }`                    |
| `departureTime`              | number      | ✅                  | Epoch milliseconds of planned departure             |
| `distanceMeters`             | number      | ✅                  | Total route length in metres                        |
| `durationSeconds`            | number      | ✅                  | Estimated travel duration in seconds                |
| `geometry.encodedPolyline`   | string      | ✅ (or coordinates) | Google-encoded polyline string                      |
| `geometry.polylinePrecision` | number      | —                  | Precision factor (5 = standard, 6 = high-precision) |
| `geometry.coordinates`       | GeoPoint\[] | ✅ (or polyline)    | Explicit coordinate list instead of polyline        |
| `waypoints`                  | GeoPoint\[] | —                  | Optional intermediate waypoints (informational)     |

<Warning>
  Either `encodedPolyline` (with `polylinePrecision`) or `coordinates` must be provided. Sending neither returns a 400 error.
</Warning>

### Response

```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>
  Save `routeId` from the registration response — it is the only key needed to poll the `/weather` endpoint throughout the journey. `expirationTime` tells you until when the session is valid; however, it is extended when segments are recalculated due to an off-schedule event (see [Off-schedule](#off-route-and-off-schedule)).
</Tip>

***

## Polling route weather

### Request

```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 }
}
```

| Field             | Type        | Required | Description                         |
| ----------------- | ----------- | -------- | ----------------------------------- |
| `routeId`         | UUID string | ✅        | Session identifier from `/register` |
| `currentLocation` | GeoPoint    | ✅        | Driver's current GPS coordinate     |

### Response

`GetRouteWeatherResponse` extends `RouteRegistrationResponse` and adds:

| Field                  | Type    | Description                                                       |
| ---------------------- | ------- | ----------------------------------------------------------------- |
| `segmentsRecalculated` | boolean | `true` if segments were recalculated due to an off-schedule event |
| `warning`              | string  | Human-readable message explaining why recalculation occurred      |

All other fields (`routeId`, `expirationTime`, `distanceUnit`, `status`, `currentSegment`, `upcomingSegments`) are identical in structure to the registration response.

***

## Response field reference

### `LocationStatus`

| Field                           | Type   | Description                                                                     |
| ------------------------------- | ------ | ------------------------------------------------------------------------------- |
| `progressPercentage`            | number | Journey completion percentage (0–100)                                           |
| `distanceTraveled`              | number | Metres traveled from route start                                                |
| `remainingDistance`             | number | Metres remaining to destination                                                 |
| `timeElapsedSeconds`            | number | Seconds since `departureTime`                                                   |
| `estimatedTimeRemainingSeconds` | number | Remaining travel time based on current pace                                     |
| `scheduleDeviationSeconds`      | number | Positive = ahead of schedule; negative = behind schedule                        |
| `expectedSegmentIndex`          | number | The segment index the driver *should* currently be in per the original schedule |
| `estimatedArrivalTime`          | number | Estimated epoch-millisecond arrival time, capped at `expirationTime`            |

<Note>
  `expectedSegmentIndex` reflects the route schedule, not GPS position. If the driver is in segment 2 but the schedule expected them to be in segment 3, this field shows `3`. Compare it with `currentSegment.index` to detect schedule drift.
</Note>

### `BaseRouteSegment` (shared by `currentSegment` and `upcomingSegments`)

| Field             | Type     | Description                                      |
| ----------------- | -------- | ------------------------------------------------ |
| `index`           | number   | Zero-based segment position in the route         |
| `h3Address`       | string   | Uber H3 cell identifier at resolution 5          |
| `eta`             | number   | Scheduled arrival epoch-milliseconds             |
| `etd`             | number   | Scheduled departure epoch-milliseconds           |
| `distanceToEntry` | number   | Cumulative metres from route start to cell entry |
| `distanceToExit`  | number   | Cumulative metres from route start to cell exit  |
| `arrivalPoint`    | GeoPoint | Coordinate where the route enters the cell       |
| `departurePoint`  | GeoPoint | Coordinate where the route exits the cell        |

### `ForecastWeatherEvents` (all segments)

| Field                      | Type   | Unit | Description                                   |
| -------------------------- | ------ | ---- | --------------------------------------------- |
| `temperature`              | number | °C   | Air temperature at 2 m                        |
| `apparentTemperature`      | number | °C   | Feels-like temperature                        |
| `humidity`                 | number | %    | Relative humidity                             |
| `cloudCover`               | number | %    | Sky cloud fraction                            |
| `windSpeed`                | number | km/h | Wind speed at 10 m                            |
| `windGust`                 | number | km/h | Maximum wind gust                             |
| `windDirection`            | number | °    | Meteorological wind direction (0 = N, 90 = E) |
| `snowFall`                 | number | cm   | Snowfall accumulation                         |
| `precipitation`            | number | mm   | Precipitation accumulation                    |
| `precipitationProbability` | number | %    | Model precipitation probability               |
| `visibility`               | number | m    | Horizontal visibility                         |

***

## Nowcast summary fields

The `currentSegment.weatherEvents` object contains three additional summary objects when nowcast data is available.

### `LightningSummary`

Real-time lightning activity derived from ground-based electromagnetic sensors.

| Field                  | Type   | Description                                                                               |
| ---------------------- | ------ | ----------------------------------------------------------------------------------------- |
| `windowMinutes`        | number | Total observation window (e.g. 60 min)                                                    |
| `bucketMinutes`        | number | Slot resolution used for aggregation (e.g. 30 min)                                        |
| `totalEventCount`      | number | All strike events within the window (in-cloud lightning + cloud-to-ground strikes)        |
| `cgFlashCount`         | number | Flash count — the most dangerous type for exposed workers and drivers                     |
| `icPulseCount`         | number | Pulse count — indicator of storm electrification intensity                                |
| `lastEventAgeSec`      | number | Seconds since the most recent event of any type                                           |
| `lastFlashAgeSec`      | number | Seconds since the most recent flash                                                       |
| `lastPulseAgeSec`      | number | Seconds since the most recent pulse                                                       |
| `nearestEventDistance` | number | Metres to the closest event of any type                                                   |
| `nearestFlashDistance` | number | Metres to the closest flash                                                               |
| `nearestPulseDistance` | number | Metres to the closest pulse                                                               |
| `maxPeakCurrent`       | number | Maximum peak current (kA) in the window — higher values indicate more destructive flashes |
| `avgPeakCurrent`       | number | Average peak current (kA)                                                                 |
| `avgSensorCount`       | number | Average number of sensors that detected each event — higher = more accurate location      |
| `confidenceScore`      | number | 0–1 confidence score of detection quality                                                 |
| `confidenceLevel`      | enum   | `LOW` / `MEDIUM` / `HIGH`                                                                 |
| `riskIndex`            | number | Composite risk score (0–100)                                                              |
| `riskLevel`            | enum   | `NONE` / `LOW` / `MEDIUM` / `HIGH` / `EXTREME`                                            |
| `trend`                | enum   | `UNKNOWN` / `DECREASING` / `STABLE` / `INCREASING`                                        |

**What this means for a mobile app user:** Show a lightning shield icon with the `riskLevel` colour. If `riskLevel` is `HIGH` or `EXTREME`, surface an urgent alert. Use `nearestFlashDistance` and `lastFlashAgeSec` to give context: *"A flash was detected 3.2 km away 45 seconds ago — stay in your vehicle."*

***

### `ThunderstormSummary`

Storm cell tracking derived from multi-layer radar analysis.

#### `summary` object

| Field                           | Type    | Description                                                                       |
| ------------------------------- | ------- | --------------------------------------------------------------------------------- |
| `stormCount`                    | number  | Total tracked storm cells in the observation window                               |
| `activeStorms`                  | number  | Cells currently producing lightning or heavy precipitation                        |
| `insideAnyThreatBoundary`       | boolean | `true` if the driver's current H3 cell overlaps any storm's threat polygon        |
| `nearestThreatBoundaryDistance` | number  | Metres to the closest storm threat boundary                                       |
| `nearestStormCentroidDistance`  | number  | Metres to the closest storm cell centre                                           |
| `maxSeverity`                   | enum    | `LOW` / `NORMAL` / `HIGH` / `UNKNOWN` — highest severity among all tracked storms |
| `lastEventAge`                  | number  | Seconds since the most recent storm event                                         |
| `riskScore`                     | number  | Composite risk score (0–100)                                                      |
| `riskLevel`                     | enum    | `NONE` / `LOW` / `MEDIUM` / `HIGH` / `EXTREME` / `UNKNOWN`                        |
| `trend`                         | enum    | `RISING` / `STABLE` / `FALLING` / `UNKNOWN`                                       |

#### `activeStorms[]` — per-storm detail

| Field                             | Type    | Description                                                                                                         |
| --------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------- |
| `eventId`                         | string  | Unique identifier of the tracked storm cell                                                                         |
| `severity`                        | enum    | `LOW` / `NORMAL` / `HIGH` / `UNKNOWN`                                                                               |
| `eventStartUtcEpoch`              | number  | Epoch-milliseconds when this storm cell was first detected                                                          |
| `eventEndUtcEpoch`                | number  | Epoch-milliseconds of last recorded activity                                                                        |
| `eventAge`                        | number  | Seconds since the storm cell was first detected                                                                     |
| `cell.area`                       | number  | Storm cell area (km²)                                                                                               |
| `cell.speed`                      | number  | Storm movement speed (km/h)                                                                                         |
| `cell.direction`                  | number  | Storm movement direction (degrees, meteorological)                                                                  |
| `cell.centroidDistance`           | number  | Metres from driver to storm cell centre                                                                             |
| `cell.directionFromDriver`        | enum    | Compass bearing: `N`, `NNE`, `NE`, `ENE`, `E`, `ESE`, `SE`, `SSE`, `S`, `SSW`, `SW`, `WSW`, `W`, `WNW`, `NW`, `NNW` |
| `cell.bearingFromDriver`          | number  | Exact bearing in degrees from driver to storm centroid                                                              |
| `flashRates.inCloud`              | number  | Pulse rate (count/min)                                                                                              |
| `flashRates.cloudToGround`        | number  | Flash rate (count/min)                                                                                              |
| `flashRates.total`                | number  | Total strike rate (count/min)                                                                                       |
| `flashRates.cloudToGroundRatio`   | number  | Flash ratio (0–1); higher = more ground strikes                                                                     |
| `threat.insideThreatPolygon`      | boolean | `true` if driver is inside this storm's threat polygon                                                              |
| `threat.distanceToThreatBoundary` | number  | Metres to this storm's threat polygon boundary                                                                      |
| `threat.directionFromDriver`      | enum    | Compass direction to threat boundary                                                                                |
| `threat.bearingFromDriver`        | number  | Exact bearing to threat boundary                                                                                    |
| `threat.approachState`            | enum    | `APPROACHING` / `MOVING_AWAY` / `STABLE` / `UNKNOWN`                                                                |
| `score.stormRiskScore`            | number  | Per-storm risk score (0–100)                                                                                        |
| `score.stormRiskLevel`            | enum    | `NONE` / `LOW` / `MEDIUM` / `HIGH` / `EXTREME` / `UNKNOWN`                                                          |

**What this means for a mobile app user:** If `insideAnyThreatBoundary` is `true`, this is an immediate safety alert — the driver is inside a storm's forecast impact zone. The `approachState: APPROACHING` flag combined with `nearestThreatBoundaryDistance` helps drivers decide whether to pull over or continue. Show the per-storm bearing so drivers understand which direction the storm is coming from.

***

### `PrecipitationSummary`

Radar-derived precipitation analysis for the current H3 cell.

| Field                  | Type    | Description                                                                                                              |
| ---------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------ |
| `windowMinutes`        | number  | Observation window in minutes                                                                                            |
| `bucketMinutes`        | number  | Aggregation slot in minutes                                                                                              |
| `currentIntensity`     | enum    | Current intensity: `DRIZZLE` / `LIGHT` / `MODERATE` / `HEAVY` / `VERY_HEAVY` / `EXTREME`, or `null` if not precipitating |
| `isPrecipitating`      | boolean | `true` if precipitation is currently detected over the cell                                                              |
| `radarTimeStamp`       | number  | Epoch-milliseconds of the radar scan used                                                                                |
| `dataAgeSec`           | number  | Age of radar data in seconds — below 600 s is considered fresh                                                           |
| `radarSnapshotCount`   | number  | Number of radar scans included in the window                                                                             |
| `maxIntensity`         | enum    | Maximum intensity recorded in the window                                                                                 |
| `trend`                | enum    | `INCREASING` / `STABLE` / `DECREASING` / `UNKNOWN`                                                                       |
| `riskLevel`            | enum    | `NONE` / `LOW` / `MEDIUM` / `HIGH` / `EXTREME`                                                                           |
| `forecastMaxIntensity` | enum    | NWP model's predicted maximum intensity for the near future                                                              |
| `expectedEndSec`       | number  | Seconds until precipitation is expected to stop (if currently precipitating)                                             |
| `expectedStartSec`     | number  | Seconds until precipitation is expected to start (if currently dry)                                                      |

**What this means for a mobile app user:** Surface `currentIntensity` as a weather badge. Use `expectedStartSec` to give advance warning — *"Heavy rain expected in 8 minutes"* — so drivers can prepare (wipers, speed reduction). `HEAVY` or above warrants a proactive push notification.

***

## Off-route and off-schedule

### Off-route

The `/weather` endpoint checks whether the driver's `currentLocation` is within **1,000 metres** of any segment of the registered route (perpendicular distance).

If the driver has deviated beyond this threshold:

* The route session is **immediately deleted** from cache.
* A **400 Bad Request** is returned with `error: "Off Route"`.
* The `routeId` is no longer valid.
* **The client must call `/register` again** with a new route.

```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 guidance:** Listen for HTTP 400 with `error: "Off Route"`. Show the user a dialog: *"You have left the route. Please start navigation on your new route to continue weather monitoring."* Do not retry `/weather` with the same `routeId`.

### Off-schedule

If the driver's GPS position is significantly behind the expected schedule — specifically, if `now` is more than 10 minutes past the scheduled ETA of the segment *after* the driver's current segment — the service silently recalculates the remaining route segments from the driver's current position.

When recalculation occurs:

* **`segmentsRecalculated: true`** is set in the response.
* **`warning`** contains a human-readable explanation.
* Segment indices, ETAs, and distances are reset from the driver's current position.
* `currentSegment` reflects the new segment 0 starting from where the driver is now.
* The `expirationTime` is extended to new ETA + 2 hours.

<Tip>
  When `segmentsRecalculated` is `true`, discard any cached segment data from previous responses and re-render the full segment list from the new response.
</Tip>

***

## Error reference

| HTTP Status | `error`               | Trigger                                                                                                                                                                                 |
| ----------- | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400         | `Off Route`           | Driver is more than 1,000 m from the route. Session deleted.                                                                                                                            |
| 400         | `Invalid Route`       | `routeId` not found, session expired, or route geometry invalid.                                                                                                                        |
| 409         | `Duplicate Route`     | A session for the same origin → destination pair is already active. Use the existing `routeId`.                                                                                         |
| 402         | `Payment Required`    | Subscription expired or exhausted.                                                                                                                                                      |
| 403         | `Forbidden`           | No subscription for this endpoint, or queried location outside subscription boundaries.                                                                                                 |
| 429         | `Too Many Requests`   | Subscription rate limit exceeded.                                                                                                                                                       |
| 500         | `Data Provider Error` | A weather data provider was unreachable. Response includes source (Nowcast / Forecast) and service (Lightning, Thunderstorm, Precipitation, Current Forecast, Hourly Forecast) context. |

### Duplicate Route (409)

```json theme={null}
{
  "timestamp": "2026-04-15T10:01:00.000000000",
  "status": 409,
  "error": "Duplicate Route",
  "messages": [
    "An active session already exists with routeId: 84863286-c5f8-4a2c-9d40-837213e8c38e",
    "This session expires at: 2026-04-15T13:54:00Z",
    "Use the existing routeId to continue polling /weather."
  ],
  "path": "/v1/enroute/register"
}
```

### Expired or Not Found (400)

```json theme={null}
{
  "timestamp": "2026-04-14T17:34:14.421986962",
  "status": 400,
  "error": "Invalid Route",
  "message": "Route not found or the route session has expired.",
  "path": "/v1/enroute/weather"
}
```

### Data Provider Error (500)

Returned when any of the underlying weather data services (lightning, thunderstorm, precipitation, or forecast) is unreachable. The `message` field identifies which source and service failed so you can log it and present a user-friendly fallback.

<Tabs>
  <Tab title="Nowcast failure">
    ```json theme={null}
    {
      "timestamp": "2026-04-15T10:00:00.000000000",
      "status": 500,
      "error": "Data Provider Error",
      "message": "A problem occurred while retrieving weather data (Nowcast Weather Events / Lightning). Please try again later.",
      "path": "/v1/enroute/register"
    }
    ```
  </Tab>

  <Tab title="Forecast failure">
    ```json theme={null}
    {
      "timestamp": "2026-04-15T10:00:00.000000000",
      "status": 500,
      "error": "Data Provider Error",
      "message": "A problem occurred while retrieving weather data (Forecast Weather Events / Hourly Forecast). Please try again later.",
      "path": "/v1/enroute/register"
    }
    ```
  </Tab>
</Tabs>

***

## Developer notes

<AccordionGroup>
  <Accordion title="All timestamps are epoch milliseconds" icon="clock">
    Every `Instant`-typed field in the API serializes as a 64-bit integer epoch millisecond — `routeId`, `expirationTime`, `eta`, `etd`, `estimatedArrivalTime`. Never parse these as seconds. Multiply by 1 and interpret directly as `Date(value)` in JavaScript or `Instant.ofEpochMilli(value)` in Java.
  </Accordion>

  <Accordion title="One active session per origin–destination pair" icon="copy">
    The API enforces deduplication on `(userId, originH3, destinationH3)`. If the driver tries to register the same route while an active session exists, a 409 Conflict is returned with the existing `routeId` and `expirationTime` in the `messages` array. Parse the `messages` array to extract the existing `routeId` and resume polling rather than treating the 409 as a fatal error.
  </Accordion>

  <Accordion title="Session expiration" icon="timer">
    Sessions expire at **ETA + 2 hours**. After expiration, calls to `/weather` with the old `routeId` return 400. Poll `/weather` before `expirationTime` to detect expiry proactively without waiting for an error.
  </Accordion>

  <Accordion title="Polling frequency" icon="rotate">
    There is no server-side push; all updates require a client-initiated `/weather` poll. Recommended polling interval: every **30–60 seconds** while the vehicle is moving. Reduce to every 5 minutes when stationary (e.g. at a traffic stop).
  </Accordion>

  <Accordion title="Handling segmentsRecalculated" icon="arrows-rotate">
    When `segmentsRecalculated: true`, the segment list has been rebuilt from the driver's current position. Segment `index` values restart from 0. Any UI element that cached previous segment indices (progress bars, segment list scrolling) must be re-initialized using the new response.
  </Accordion>

  <Accordion title="distanceUnit is always metres" icon="ruler">
    The `distanceUnit` field in the response is always `"m"`. All `distanceToEntry`, `distanceToExit`, and `remainingDistance` values are in metres. Convert to km or miles in your UI layer.
  </Accordion>
</AccordionGroup>

***

## Testing with Postman

The Postman visualization script below renders a human-readable route summary directly in the **Visualize** tab after each `/register` or `/weather` request — no need to read raw JSON to understand the current route state.

**How to use:**

1. Open the request in Postman (`POST /v1/enroute/register` or `POST /v1/enroute/weather`).
2. Go to **Scripts** → **Post-response** and paste the script below.
3. Send the request, then switch to the **Visualize** tab to see the summary.

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