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