Skip to main content

⚡ Real-time nowcast

The current segment always carries live radar-derived lightning, thunderstorm, and precipitation data — not model estimates.

🔭 Forecast ahead

Upcoming segments are enriched with hourly NWP forecast weather so drivers can plan for conditions hours away.

🗺️ Smart segmentation

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.

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:
EndpointPurpose
POST /v1/enroute/registerCreate a route session. Returns a routeId and the initial weather picture.
POST /v1/enroute/weatherPoll an active session with the driver’s current location. Returns updated weather, position status, and any schedule changes.
All timestamps in requests and responses are epoch milliseconds (64-bit integers). Never send ISO strings or fractional seconds.

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)
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.
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:
ObjectSourceWhat it tells you
lightningSummaryLightning detection sensorsFlash counts, peak current, risk index, approach trend
thunderstormSummaryRadar storm tracking cellsActive storm list, threat boundary proximity, bearing, severity
precipitationSummaryRadar reflectivityCurrent 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.
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.

Registering a route

Request

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
  }
}
FieldTypeRequiredDescription
originGeoPointStarting coordinate { lat, lng }
destinationGeoPointEnding coordinate { lat, lng }
departureTimenumberEpoch milliseconds of planned departure
distanceMetersnumberTotal route length in metres
durationSecondsnumberEstimated travel duration in seconds
geometry.encodedPolylinestring✅ (or coordinates)Google-encoded polyline string
geometry.polylinePrecisionnumberPrecision factor (5 = standard, 6 = high-precision)
geometry.coordinatesGeoPoint[]✅ (or polyline)Explicit coordinate list instead of polyline
waypointsGeoPoint[]Optional intermediate waypoints (informational)
Either encodedPolyline (with polylinePrecision) or coordinates must be provided. Sending neither returns a 400 error.

Response

{
  "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
      }
    }
  ]
}
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).

Polling route weather

Request

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

{
  "routeId": "84863286-c5f8-4a2c-9d40-837213e8c38e",
  "currentLocation": { "lat": 40.9871, "lng": 39.6543 }
}
FieldTypeRequiredDescription
routeIdUUID stringSession identifier from /register
currentLocationGeoPointDriver’s current GPS coordinate

Response

GetRouteWeatherResponse extends RouteRegistrationResponse and adds:
FieldTypeDescription
segmentsRecalculatedbooleantrue if segments were recalculated due to an off-schedule event
warningstringHuman-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

FieldTypeDescription
progressPercentagenumberJourney completion percentage (0–100)
distanceTravelednumberMetres traveled from route start
remainingDistancenumberMetres remaining to destination
timeElapsedSecondsnumberSeconds since departureTime
estimatedTimeRemainingSecondsnumberRemaining travel time based on current pace
scheduleDeviationSecondsnumberPositive = ahead of schedule; negative = behind schedule
expectedSegmentIndexnumberThe segment index the driver should currently be in per the original schedule
estimatedArrivalTimenumberEstimated epoch-millisecond arrival time, capped at expirationTime
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.

BaseRouteSegment (shared by currentSegment and upcomingSegments)

FieldTypeDescription
indexnumberZero-based segment position in the route
h3AddressstringUber H3 cell identifier at resolution 5
etanumberScheduled arrival epoch-milliseconds
etdnumberScheduled departure epoch-milliseconds
distanceToEntrynumberCumulative metres from route start to cell entry
distanceToExitnumberCumulative metres from route start to cell exit
arrivalPointGeoPointCoordinate where the route enters the cell
departurePointGeoPointCoordinate where the route exits the cell

ForecastWeatherEvents (all segments)

FieldTypeUnitDescription
temperaturenumber°CAir temperature at 2 m
apparentTemperaturenumber°CFeels-like temperature
humiditynumber%Relative humidity
cloudCovernumber%Sky cloud fraction
windSpeednumberkm/hWind speed at 10 m
windGustnumberkm/hMaximum wind gust
windDirectionnumber°Meteorological wind direction (0 = N, 90 = E)
snowFallnumbercmSnowfall accumulation
precipitationnumbermmPrecipitation accumulation
precipitationProbabilitynumber%Model precipitation probability
visibilitynumbermHorizontal 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.
FieldTypeDescription
windowMinutesnumberTotal observation window (e.g. 60 min)
bucketMinutesnumberSlot resolution used for aggregation (e.g. 30 min)
totalEventCountnumberAll strike events within the window (in-cloud lightning + cloud-to-ground strikes)
cgFlashCountnumberFlash count — the most dangerous type for exposed workers and drivers
icPulseCountnumberPulse count — indicator of storm electrification intensity
lastEventAgeSecnumberSeconds since the most recent event of any type
lastFlashAgeSecnumberSeconds since the most recent flash
lastPulseAgeSecnumberSeconds since the most recent pulse
nearestEventDistancenumberMetres to the closest event of any type
nearestFlashDistancenumberMetres to the closest flash
nearestPulseDistancenumberMetres to the closest pulse
maxPeakCurrentnumberMaximum peak current (kA) in the window — higher values indicate more destructive flashes
avgPeakCurrentnumberAverage peak current (kA)
avgSensorCountnumberAverage number of sensors that detected each event — higher = more accurate location
confidenceScorenumber0–1 confidence score of detection quality
confidenceLevelenumLOW / MEDIUM / HIGH
riskIndexnumberComposite risk score (0–100)
riskLevelenumNONE / LOW / MEDIUM / HIGH / EXTREME
trendenumUNKNOWN / 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

FieldTypeDescription
stormCountnumberTotal tracked storm cells in the observation window
activeStormsnumberCells currently producing lightning or heavy precipitation
insideAnyThreatBoundarybooleantrue if the driver’s current H3 cell overlaps any storm’s threat polygon
nearestThreatBoundaryDistancenumberMetres to the closest storm threat boundary
nearestStormCentroidDistancenumberMetres to the closest storm cell centre
maxSeverityenumLOW / NORMAL / HIGH / UNKNOWN — highest severity among all tracked storms
lastEventAgenumberSeconds since the most recent storm event
riskScorenumberComposite risk score (0–100)
riskLevelenumNONE / LOW / MEDIUM / HIGH / EXTREME / UNKNOWN
trendenumRISING / STABLE / FALLING / UNKNOWN

activeStorms[] — per-storm detail

FieldTypeDescription
eventIdstringUnique identifier of the tracked storm cell
severityenumLOW / NORMAL / HIGH / UNKNOWN
eventStartUtcEpochnumberEpoch-milliseconds when this storm cell was first detected
eventEndUtcEpochnumberEpoch-milliseconds of last recorded activity
eventAgenumberSeconds since the storm cell was first detected
cell.areanumberStorm cell area (km²)
cell.speednumberStorm movement speed (km/h)
cell.directionnumberStorm movement direction (degrees, meteorological)
cell.centroidDistancenumberMetres from driver to storm cell centre
cell.directionFromDriverenumCompass bearing: N, NNE, NE, ENE, E, ESE, SE, SSE, S, SSW, SW, WSW, W, WNW, NW, NNW
cell.bearingFromDrivernumberExact bearing in degrees from driver to storm centroid
flashRates.inCloudnumberPulse rate (count/min)
flashRates.cloudToGroundnumberFlash rate (count/min)
flashRates.totalnumberTotal strike rate (count/min)
flashRates.cloudToGroundRationumberFlash ratio (0–1); higher = more ground strikes
threat.insideThreatPolygonbooleantrue if driver is inside this storm’s threat polygon
threat.distanceToThreatBoundarynumberMetres to this storm’s threat polygon boundary
threat.directionFromDriverenumCompass direction to threat boundary
threat.bearingFromDrivernumberExact bearing to threat boundary
threat.approachStateenumAPPROACHING / MOVING_AWAY / STABLE / UNKNOWN
score.stormRiskScorenumberPer-storm risk score (0–100)
score.stormRiskLevelenumNONE / 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.
FieldTypeDescription
windowMinutesnumberObservation window in minutes
bucketMinutesnumberAggregation slot in minutes
currentIntensityenumCurrent intensity: DRIZZLE / LIGHT / MODERATE / HEAVY / VERY_HEAVY / EXTREME, or null if not precipitating
isPrecipitatingbooleantrue if precipitation is currently detected over the cell
radarTimeStampnumberEpoch-milliseconds of the radar scan used
dataAgeSecnumberAge of radar data in seconds — below 600 s is considered fresh
radarSnapshotCountnumberNumber of radar scans included in the window
maxIntensityenumMaximum intensity recorded in the window
trendenumINCREASING / STABLE / DECREASING / UNKNOWN
riskLevelenumNONE / LOW / MEDIUM / HIGH / EXTREME
forecastMaxIntensityenumNWP model’s predicted maximum intensity for the near future
expectedEndSecnumberSeconds until precipitation is expected to stop (if currently precipitating)
expectedStartSecnumberSeconds 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.
{
  "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.
When segmentsRecalculated is true, discard any cached segment data from previous responses and re-render the full segment list from the new response.

Error reference

HTTP StatuserrorTrigger
400Off RouteDriver is more than 1,000 m from the route. Session deleted.
400Invalid RouterouteId not found, session expired, or route geometry invalid.
409Duplicate RouteA session for the same origin → destination pair is already active. Use the existing routeId.
402Payment RequiredSubscription expired or exhausted.
403ForbiddenNo subscription for this endpoint, or queried location outside subscription boundaries.
429Too Many RequestsSubscription rate limit exceeded.
500Data Provider ErrorA weather data provider was unreachable. Response includes source (Nowcast / Forecast) and service (Lightning, Thunderstorm, Precipitation, Current Forecast, Hourly Forecast) context.

Duplicate Route (409)

{
  "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)

{
  "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.
{
  "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"
}

Developer notes

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

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 ScriptsPost-response and paste the script below.
  3. Send the request, then switch to the Visualize tab to see the summary.
// 1. Response (Cevap) Verisini Al
const responseData = pm.response.json();

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

              'NORMAL': 'Normal',

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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