1(function () { 2 function formatUtc(value) { 3 const date = new Date(value); 4 5 if (Number.isNaN(date.getTime())) { 6 return value; 7 } 8 9 return `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}-${String(date.getUTCDate()).padStart(2, '0')} ${String(date.getUTCHours()).padStart(2, '0')}:${String(date.getUTCMinutes()).padStart(2, '0')}:${String(date.getUTCSeconds()).padStart(2, '0')} UTC`; 10 } 11 12 function getLabels() { 13 const language = (document.documentElement.lang || 'en').split('-')[0].toLowerCase(); 14 const labels = { 15 en: { 16 earthquake: 'Earthquake', 17 noMappable: 'No mappable earthquakes are available right now.', 18 mapError: 'The map could not load earthquake markers right now.', 19 magnitude: 'Magnitude', 20 depth: 'Depth', 21 time: 'Time', 22 country: 'Country', 23 openDetail: 'Open detail page' 24 }, 25 fr: { 26 earthquake: 'Séisme', 27 noMappable: 'Aucun séisme cartographiable nâest disponible pour le moment.', 28 mapError: 'La carte ne peut pas charger les marqueurs de séismes pour le moment.', 29 magnitude: 'Magnitude', 30 depth: 'Profondeur', 31 time: 'Heure', 32 country: 'Pays', 33 openDetail: 'Voir le détail' 34 }, 35 es: { 36 earthquake: 'Terremoto', 37 noMappable: 'No hay terremotos cartografiables disponibles ahora.', 38 mapError: 'El mapa no puede cargar los marcadores de terremotos ahora.', 39 magnitude: 'Magnitud', 40 depth: 'Profundidad', 41 time: 'Hora', 42 country: 'PaÃs', 43 openDetail: 'Ver detalle' 44 }, 45 ja: { 46 earthquake: 'å°é', 47 noMappable: 'ç¾å¨ãå°å³ã«è¡¨ç¤ºã§ããå°éã¯ããã¾ããã', 48 mapError: 'ç¾å¨ãå°éãã¼ã«ã¼ãèªã¿è¾¼ãã¾ããã', 49 magnitude: 'ãã°ããã¥ã¼ã', 50 depth: 'æ·±ã', 51 time: 'æå»', 52 country: 'å½', 53 openDetail: '詳細ãè¦ã' 54 } 55 }; 56 57 return labels[language] || labels.en; 58 } 59 60 function escapeHtml(value) { 61 return String(value || '') 62 .replace(/&/g, '&') 63 .replace(/</g, '<') 64 .replace(/>/g, '>') 65 .replace(/"/g, '"') 66 .replace(/'/g, '''); 67 } 68 69 function buildDetailsUrl(earthquake, template) { 70 const occurredAt = new Date(earthquake.occurredAtUtc); 71 72 if (Number.isNaN(occurredAt.getTime())) { 73 return '#'; 74 } 75 76 const timeSegment = [ 77 String(occurredAt.getUTCHours()).padStart(2, '0'), 78 String(occurredAt.getUTCMinutes()).padStart(2, '0'), 79 String(occurredAt.getUTCSeconds()).padStart(2, '0'), 80 String(occurredAt.getUTCMilliseconds()).padStart(3, '0') 81 ].join('-'); 82 83 return template 84 .replace('YYYY', String(occurredAt.getUTCFullYear())) 85 .replace('MM', String(occurredAt.getUTCMonth() + 1).padStart(2, '0')) 86 .replace('DD', String(occurredAt.getUTCDate()).padStart(2, '0')) 87 .replace('TIME', timeSegment); 88 } 89 90 function createMarkerIcon(earthquake) { 91 const magnitude = Number(earthquake.magnitude) || 0; 92 const size = Math.max(28, Math.min(46, 14 + magnitude * 3.5)); 93 94 let accentClass = 'bg-emerald-500/85 border-emerald-700/70'; 95 if (magnitude >= 7) { 96 accentClass = 'bg-rose-600/90 border-rose-900/80'; 97 } else if (magnitude >= 6) { 98 accentClass = 'bg-orange-500/90 border-orange-900/80'; 99 } else if (magnitude >= 5) { 100 accentClass = 'bg-amber-400/90 border-amber-800/80'; 101 } 102 103 return L.divIcon({ 104 className: 'earthquake-marker-wrapper', 105 html: `<div class="flex items-center justify-center rounded-full border-2 text-[11px] font-black text-white shadow-lg ${accentClass}" style="width:${size}px;height:${size}px;">${magnitude.toFixed(1)}</div>`, 106 iconSize: [size, size], 107 iconAnchor: [size / 2, size / 2], 108 popupAnchor: [0, -(size / 2)] 109 }); 110 } 111 112 async function bootRealtimeMap() { 113 const mapElement = document.getElementById('earthquakes-realtime-map'); 114 if (!mapElement || typeof window.L === 'undefined') { 115 return; 116 } 117 118 const apiUrl = mapElement.dataset.earthquakesApiUrl; 119 const detailsTemplate = mapElement.dataset.detailsUrlTemplate || '/YYYY/MM/DD/TIME'; 120 const labels = getLabels(); 121 122 const map = L.map(mapElement, { 123 scrollWheelZoom: false 124 }
124).setView([20, 0], 2); 125 126 L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { 127 detectRetina: true, 128 maxZoom: 18, 129 attribution: '© OpenStreetMap contributors' 130 }).addTo(map); 131 132 try { 133 const response = await fetch(apiUrl, { 134 headers: { 135 Accept: 'application/json' 136 } 137 }); 138 139 if (!response.ok) { 140 throw new Error(`HTTP ${response.status}`); 141 } 142 143 const earthquakes = await response.json(); 144 const validEarthquakes = earthquakes.filter((earthquake) => 145 Number.isFinite(Number(earthquake.latitude)) && 146 Number.isFinite(Number(earthquake.longitude))) 147 .slice(0, 60); 148 149 if (!validEarthquakes.length) { 150 mapElement.insertAdjacentHTML( 151 'beforeend', 152 `<div class="pointer-events-none absolute inset-0 flex items-center justify-center bg-white/70 text-sm font-semibold text-slate-700">${escapeHtml(labels.noMappable)}</div>`); 153 return; 154 } 155 156 const bounds = []; 157 158 validEarthquakes.forEach((earthquake) => { 159 const latitude = Number(earthquake.latitude); 160 const longitude = Number(earthquake.longitude); 161 const detailsUrl = buildDetailsUrl(earthquake, detailsTemplate); 162 const liveLabel = earthquake.region || earthquake.title || labels.earthquake; 163 const title = earthquake.title && earthquake.title !== liveLabel ? earthquake.title : ''; 164 const canOpenEarthquake = Number(earthquake.magnitude) >= 5; 165 166 bounds.push([latitude, longitude]); 167 168 const marker = L.marker([latitude, longitude], { 169 icon: createMarkerIcon(earthquake), 170 keyboard: false, 171 interactive: canOpenEarthquake 172 }).addTo(map); 173 174 const markerElement = marker.getElement(); 175 markerElement?.removeAttribute('tabindex'); 176 markerElement?.removeAttribute('role'); 177 178 if (!canOpenEarthquake) { 179 return; 180 } 181 182 marker.bindPopup(` 183 <div style="min-width: 220px;"> 184 <div style="font-weight: 800; font-size: 1rem; margin-bottom: 0.35rem;">${escapeHtml(liveLabel)}</div> 185 ${title ? `<div style="font-size: 0.875rem; color: #475569; margin-bottom: 0.5rem;">${escapeHtml(title)}</div>` : ''} 186 <div style="display: grid; gap: 0.2rem; font-size: 0.875rem;"> 187 <div><strong>${escapeHtml(labels.magnitude)}:</strong> ${Number(earthquake.magnitude).toFixed(1)}</div> 188 <div><strong>${escapeHtml(labels.country)}:</strong> ${escapeHtml(earthquake.country || '')}</div> 189 <div><strong>${escapeHtml(labels.depth)}:</strong> ${Number(earthquake.depthKm).toFixed(1)} km</div> 190 <div><strong>${escapeHtml(labels.time)}:</strong> ${escapeHtml(formatUtc(earthquake.occurredAtUtc))}</div> 191 </div> 192 ${canOpenEarthquake ? `<div style="margin-top: 0.65rem;"> 193 <a href="${escapeHtml(detailsUrl)}" style="font-weight: 700; color: #c2410c; text-decoration: none;">${escapeHtml(labels.openDetail)}</a> 194 </div>` : ''} 195 </div> 196 `); 197 }); 198 199 if (bounds.length === 1) {
200 map.setView(bounds[0], 5); 201 } else { 202 map.fitBounds(bounds, { padding: [28, 28] }); 203 } 204 } catch (error) { 205 console.error('Failed to initialize earthquake map.', error); 206 mapElement.insertAdjacentHTML( 207 'beforeend', 208 `<div class="pointer-events-none absolute inset-0 flex items-center justify-center bg-white/70 px-6 text-center text-sm font-semibold text-slate-700">${escapeHtml(labels.mapError)}</div>`); 209 } 210 } 211 212 if (document.readyState === 'loading') { 213 document.addEventListener('DOMContentLoaded', bootRealtimeMap); 214 } else { 215 bootRealtimeMap(); 216 } 217})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.