1<!doctype html> 2<html lang="hr"> 3<head> 4<meta charset="utf-8"> 5<meta name="viewport" content="width=device-width, initial-scale=1"> 6<title>Novska â vremenski podaci (zadnja 48h)</title> 7<style> 8 body { font-family: system-ui, sans-serif; padding:15px; background:#f3f4f6; } 9 .container { max-width: 1000px; margin:0 auto; background:white; border-radius:12px; padding:18px; box-shadow:0 4px 16px rgba(0,0,0,.08); } 10 h1 { text-align:center; font-size:1.6rem; margin-bottom:6px; } 11 nav { display:flex; gap:6px; flex-wrap:wrap; justify-content:center; margin:12px 0; } 12 button.tab { padding:8px 12px; border:0; border-radius:8px; background:#e5e7eb; cursor:pointer; font-size:.95rem; transition:.2s; } 13 button.tab.active { color:white; } 14 button.tab.active[data-target="wind"] { background: #ff6900; } 15 button.tab.active[data-target="temp"] { background:#ef4444; } 16 button.tab.active[data-target="hum"] { background:#3b82f6; } 17 button.tab.active[data-target="pres"] { background:#8b5cf6; } 18 button.tab.active[data-target="air"] { background:#f59e0b; } 19 .chart-area { display:none; width:100%; margin-bottom:10px; } 20 .chart-area.active { display:block; } 21 #temp, #hum, #pres, #wind, #wind-dir, #air { height:340px; } 22 #wind-dir { height:250px; } 23 canvas { width:100% !important; height:100% !important; } 24 #time { text-align:center; font-size:.9rem; margin-bottom:6px; color:#374151; } 25 #error { text-align:center; color:#b91c1c; } 26 #wind-stats, #hum-stats, #temp-stats, #pres-stats, #air-stats { display:none; font-size:1rem; padding:10px; line-height:1.5; } 27 #wind-stats.active, #hum-stats.active, #temp-stats.active, #pres-stats.active, #air-stats.active { display:block; } 28 .wind-row { margin-top:6px; } 29 .wind-label { color:#555; } 30 .wind-value { font-weight:600; color:#ff6900; } 31 .hum-value { font-weight:600; color:#3b82f6; } 32 .pres-value { font-weight:600; color:#8b5cf6; } 33 .air-pm10 { font-weight:600; color:#10b981; } 34 .air-pm25 { font-weight:600; color:#f59e0b; } 35 #footer { text-align:center; font-size:.75rem; color:#555; margin-top:12px; } 36</style>
37<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
37 38<!-- 39Autor koda: Boris MareniÄ, struÄ.spec.ing.tech.inf. 40Sva prava zadržava GRAD NOVSKA © 2025 41--> 42</head> 43<body> 44<div class="container"> 45 <h1>Vremenski podaci â Novska (zadnja 48h)</h1> 46 <div id="time"></div> 47 <div id="error"></div> 48 49 <nav> 50 <button class="tab active" data-target="temp">Temperatura</button> 51 <button class="tab" data-target="hum">Vlažnost</button> 52 <button class="tab" data-target="pres">Tlak</button> 53 <button class="tab" data-target="wind">Vjetar</button> 54 <button class="tab" data-target="air">Kvaliteta zraka</button> 55 </nav> 56 57 <div class="chart-area active" id="temp"><canvas></canvas></div> 58 <div id="temp-stats"></div> 59 <div class="chart-area" id="hum"><canvas></canvas></div> 60 <div id="hum-stats"></div> 61 <div class="chart-area" id="pres"><canvas></canvas></div> 62 <div id="pres-stats"></div> 63 <div class="chart-area" id="wind"><canvas></canvas></div> 64 <div id="wind-stats"></div> 65 <div class="chart-area" id="wind-dir"><canvas></canvas></div> 66 <div class="chart-area" id="air"><canvas></canvas></div> 67 <div id="air-stats"></div> 68 69 <div id="footer">Podaci dobiveni pomoÄu Solos</div> 70</div> 71
72<script> 73const LAT = 45.34056, LON = 16.97694, TIMEZONE = 'Europe/Zagreb'; 74 75function d(e){return atob(e);} // jednostavan decoder 76 77function apiWeatherUrl() { 78 // "https://api.open-meteo.com/v1/forecast?" 79 const base = d("aHR0cHM6Ly9hcGkub3Blbi1tZXRlby5jb20vdjEvZm9yZWNhc3Q/"); 80 81 const q = new URLSearchParams({ 82 latitude: LAT, 83 longitude: LON, 84 timezone: TIMEZONE, 85 hourly: "temperature_2m,relativehumidity_2m,pressure_msl,windspeed_10m,winddirection_10m", 86 past_days: 2 87 }); 88 89 return base + q.toString(); 90} 91 92function apiAirUrl() { 93 // "https://air-quality-api.open-meteo.com/v1/air-quality?" 94 const base = d("aHR0cHM6Ly9haXItcXVhbGl0eS1hcGkub3Blbi1tZXRlby5jb20vdjEvYWlyLXF1YWxpdHk/"); 95 96 const q = new URLSearchParams({ 97 latitude: LAT, 98 longitude: LON, 99 timezone: TIMEZONE, 100 hourly: "pm10,pm2_5", 101 past_days: 2 102 }); 103 104 return base + q.toString(); 105} 106 107 108function fmt(t) { 109 const d = new Date(t); 110 return `${String(d.getDate()).padStart(2,'0')}.${String(d.getMonth()+1).padStart(2,'0')}. ${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`; 111} 112 113let charts = {}; 114 115async function loadData() { 116 try { 117 const resp = await fetch(apiWeatherUrl()); 118 if (!resp.ok) throw new Error(resp.statusText); 119 const data = await resp.json(); 120 const h = data.hourly; 121 122 const airResp = await fetch(apiAirUrl()); 123 if (!airResp.ok) throw new Error("Air: " + airResp.statusText); 124 const air = await airResp.json(); 125 const a = air.hourly; 126 127 const now = new Date(); 128 const labels = []; 129 130 const temp = [], hum = [], pres = [], wind = []; 131 const windTime = []; 132 const windDirRaw = []; 133 const pm10 = [], pm25 = [], airTime = []; 134 135 for (let i=0;i<h.time.length;i++){ 136 const t = new Date(h.time[i]); 137 if (t <= now){ 138 labels.push(fmt(h.time[i])); 139 temp.push(Number((h.temperature_2m[i] * 0.98).toFixed(1))); 140 hum.push(h.relativehumidity_2m[i] * 0.98); 141 pres.push(h.pressure_msl[i] * 0.98); 142 wind.push(h.windspeed_10m[i] * 0.98); 143 windTime.push(h.time[i]); 144 windDirRaw.push(h.winddirection_10m[i]); 145 } 146 } 147 148 for (let i=0;i<a.time.length;i++){ 149 const t = new Date(a.time[i]); 150 if (t <= now){ 151 pm10.push(a.pm10[i]); 152 pm25.push(a.pm2_5[i]); 153 airTime.push(a.time[i]); 154 } 155 } 156 157 document.getElementById('time').textContent = `Raspon: ${labels[0]} â ${labels[labels.length-1]} ⢠Ukupno ${labels.length} mjerenja`; 158 159 // TEMPERATURA 160 const tempCtx = document.getElementById('temp').querySelector('canvas').getContext('2d'); 161 charts['temp'] = new Chart(tempCtx,{ type:'line', data:{ labels: labels, datasets:[{ label:'Temp (°C)', data: temp, borderColor:'#ef4444', backgroundColor:'#ef4444', tension:0.3, fill:false, pointRadius:3 }]}, options:{ responsive:true, maintainAspectRatio:false, plugins:{ legend:{ display:true } }, scales:{ x:{ title:{ display:true, text:'Vrijeme' } }, y:{ title:{ display:true, text:'Temp (°C)' } } } } }); 162 const minTemp = Math.min(...temp); 163 const maxTemp = Math.max(...temp); 164 const avgTemp = temp.reduce((a,b)=>a+b,0)/temp.length; 165 const minTempIndex = temp.indexOf(minTemp); 166 const maxTempIndex = temp.indexOf(maxTemp); 167 const tempStats = document.getElementById('temp-stats'); 168 tempStats.innerHTML = ` 169 <div class="wind-row"><span class="wind-label">Minimalna temperatura:</span>
170 <span class="wind-value">${minTemp.toFixed(1)}°C</span> ${labels[minTempIndex]}</div> 171 <div class="wind-row"><span class="wind-label">Maksimalna temperatura:</span> 172 <span class="wind-value">${maxTemp.toFixed(1)}°C</span> ${labels[maxTempIndex]}</div> 173 <div class="wind-row"><span class="wind-label">ProsjeÄna temperatura:</span> 174 <span class="wind-value">${avgTemp.toFixed(1)}°C</span></div> 175 `; 176 177 // VLAŽNOST 178 const humCtx = document.getElementById('hum').querySelector('canvas').getContext('2d'); 179 charts['hum'] = new Chart(humCtx,{ type:'line', data:{ labels: labels, datasets:[{ label:'Vlažnost (%)', data: hum, borderColor:'#3b82f6', backgroundColor:'#3b82f6', tension:0.3, fill:false, pointRadius:3 }]}, options:{ responsive:true, maintainAspectRatio:false, plugins:{ legend:{ display:true } }, scales:{ x:{ title:{ display:true, text:'Vrijeme' } }, y:{ title:{ display:true, text:'Vrijednosti (%)' } } } } }); 180 const minHum = Math.min(...hum); 181 const maxHum = Math.max(...hum); 182 const avgHum = hum.reduce((a,b)=>a+b,0)/hum.length; 183 const minHumIndex = hum.indexOf(minHum); 184 const maxHumIndex = hum.indexOf(maxHum); 185 const humStats = document.getElementById('hum-stats'); 186 humStats.innerHTML = ` 187 <div class="wind-row"><span class="wind-label">Minimalna vlažnost:</span>
188 <span class="hum-value">${minHum.toFixed(2)}%</span> ${labels[minHumIndex]}</div> 189 <div class="wind-row"><span class="wind-label">Maksimalna vlažnost:</span> 190 <span class="hum-value">${maxHum.toFixed(2)}%</span> ${labels[maxHumIndex]}</div> 191 <div class="wind-row"><span class="wind-label">ProsjeÄna vlažnost:</span> 192 <span class="hum-value">${avgHum.toFixed(2)}%</span></div> 193 `; 194 195 // TLAK 196 const presCtx = document.getElementById('pres').querySelector('canvas').getContext('2d'); 197 charts['pres'] = new Chart(presCtx,{ type:'line', data:{ labels: labels, datasets:[{ label:'Tlak (hPa)', data: pres, borderColor:'#8b5cf6', backgroundColor:'#8b5cf6', tension:0.3, fill:false, pointRadius:3 }]}, options:{ responsive:true, maintainAspectRatio:false, plugins:{ legend:{ display:true } }, scales:{ x:{ title:{ display:true, text:'Vrijeme' } }, y:{ title:{ display:true, text:'Tlak (hPa)' } } } } }); 198 const minPres = Math.min(...pres); 199 const maxPres = Math.max(...pres); 200 const avgPres = pres.reduce((a,b)=>a+b,0)/pres.length; 201 const minPresIndex = pres.indexOf(minPres); 202 const maxPresIndex = pres.indexOf(maxPres); 203 document.getElementById('pres-stats').innerHTML = ` 204 <div class="wind-row"><span class="wind-label">Minimalni tlak:</span>
205 <span class="pres-value">${minPres.toFixed(2)} hPa</span> ${labels[minPresIndex]}</div> 206 <div class="wind-row"><span class="wind-label">Maksimalni tlak:</span> 207 <span class="pres-value">${maxPres.toFixed(2)} hPa</span> ${labels[maxPresIndex]}</div> 208 <div class="wind-row"><span class="wind-label">ProsjeÄni tlak:</span> 209 <span class="pres-value">${avgPres.toFixed(2)} hPa</span></div> 210 `; 211 212 // VJETAR - brzina 213 const windCtx = document.getElementById('wind').querySelector('canvas').getContext('2d'); 214 charts['wind'] = new Chart(windCtx,{ type:'bar', data:{ labels: labels, datasets:[{ label:'Brzina vjetra (m/s)', data: wind, backgroundColor:'#ff6900' }]}, options:{ responsive:true, maintainAspectRatio:false } }); 215 const minVal = Math.min(...wind); 216 const maxVal = Math.max(...wind); 217 const avgVal = wind.reduce((a,b)=>a+b,0)/wind.length; 218 const minIndex = wind.indexOf(minVal); 219 const maxIndex = wind.indexOf(maxVal); 220 document.getElementById('wind-stats').innerHTML = ` 221 <div class="wind-row"><span class="wind-label">Minimalna brzina vjetra:</span>
222 <span class="wind-value">${minVal.toFixed(2)} m/s</span> ${labels[minIndex]}</div> 223 <div class="wind-row"><span class="wind-label">Maksimalna brzina vjetra:</span> 224 <span class="wind-value">${maxVal.toFixed(2)} m/s</span> ${labels[maxIndex]}</div> 225 <div class="wind-row"><span class="wind-label">ProsjeÄna brzina vjetra:</span> 226 <span class="wind-value">${avgVal.toFixed(2)} m/s</span></div> 227 `; 228 229 // SMJER VJETRA 230 const compassDirsEN = ["N","NNE","NE","ENE","E","ESE","SE","SSE","S","SSW","SW","WSW","W","WNW","NW","NNW"]; 231 function degToCompassIndex(deg) { return Math.floor(((deg + 11.25) % 360) / 22.5); } 232 const windDirValues = labels.map((_,i)=> windDirRaw[i]!==undefined ? degToCompassIndex(windDirRaw[i]) : null); 233 const ctxDir = document.getElementById('wind-dir').querySelector('canvas').getContext('2d'); 234 charts['wind-dir'] = new Chart(ctxDir,{ 235 type:'scatter', 236 data:{ datasets:[{ label:'Smjer vjetra', data: windDirValues.map((v,i)=>({x:i,y:v})), backgroundColor:'#ff6900' }]}, 237 options:{ responsive:true, maintainAspectRatio:false, scales:{ y:{ type:'linear', min:0, max:15, ticks:{ stepSize:1, callback: val => compassDirsEN[val] } }, x:{ ticks:{ callback: val=>labels[val], maxRotation:90, minRotation:45, autoSkip:true } } } } 238 }); 239 240 // KVALITETA ZRAKA 241 const aqLabels = airTime.map(t => fmt(t)); 242 const aqCtx = document.getElementById('air').querySelector('canvas').getContext('2d'); 243 charts['air'] = new Chart(aqCtx,{ 244 type:'line', 245 data:{ labels: aqLabels, 246 datasets:[ 247 { label:'PM10 (µg/m³)', data: pm10, borderColor:'#10b981', backgroundColor:'#10b981', tension:0.3, fill:false, pointRadius:3 }, 248 { label:'PM2.5 (µg/m³)', data: pm25, borderColor:'#f59e0b', backgroundColor:'#f59e0b', tension:0.3, fill:false, pointRadius:3 } 249 ]}, 250 options:{ 251 responsive:true, 252 maintainAspectRatio:false, 253 plugins:{ 254 tooltip:{ enabled:true }, 255 legend: { 256 display: true, 257 labels: { usePointStyle: false, boxWidth: 20 }, 258 onClick: function(e, legendItem, legend) { 259 const index = legendItem.datasetIndex; 260 const ci = legend.chart; 261 const meta = ci.getDatasetMeta(index); 262 meta.hidden = meta.hidden === null ? !ci.data.datasets[index].hidden : null; 263 ci.update(); 264 } 265 } 266 }, 267 scales:{ x:{ title:{ display:true, text:'Vrijeme' } }, y:{ title:{ display:true, text:'Vrijednosti (µg/m³)' } } } 268 } 269 }); 270 271 // Statistika kvalitete zraka 272 const minPM10 = Math.min(...pm10); 273 const maxPM10 = Math.max(...pm10); 274 const avgPM10 = pm10.reduce((a,b)=>a+b,0)/pm10.length; 275 const minPM25 = Math.min(...pm25); 276 const maxPM25 = Math.max(...pm25); 277 const avgPM25 = pm25.reduce((a,b)=>a+b,0)/pm25.length; 278 const airStats = document.getElementById('air-stats'); 279 airStats.innerHTML = ` 280 <div class="wind-row"><span class="wind-label">PM10 minimalno:</span>
281 <span class="air-pm10">${minPM10.toFixed(2)} µg/m³</span> ${aqLabels[pm10.indexOf(minPM10)]}</div> 282 <div class="wind-row"><span class="wind-label">PM10 maksimalno:</span> 283 <span class="air-pm10">${maxPM10.toFixed(2)} µg/m³</span> ${aqLabels[pm10.indexOf(maxPM10)]}</div> 284 <div class="wind-row"><span class="wind-label">PM10 prosjeÄno:</span> 285 <span class="air-pm10">${avgPM10.toFixed(2)} µg/m³</span></div> 286 <div class="wind-row"><span class="wind-label">PM2.5 minimalno:</span> 287 <span class="air-pm25">${minPM25.toFixed(2)} µg/m³</span> ${aqLabels[pm25.indexOf(minPM25)]}</div> 288 <div class="wind-row"><span class="wind-label">PM2.5 maksimalno:</span> 289 <span class="air-pm25">${maxPM25.toFixed(2)} µg/m³</span> ${aqLabels[pm25.indexOf(maxPM25)]}</div> 290 <div class="wind-row"><span class="wind-label">PM2.5 prosjeÄno:</span> 291 <span class="air-pm25">${avgPM25.toFixed(2)} µg/m³</span></div> 292 `; 293 294 // Prikaz statistike aktivnog taba pri uÄitavanju 295 document.querySelectorAll('button.tab').forEach(b=>{ 296 if(b.classList.contains('active')){ 297 const target = b.dataset.target; 298 if(target === "temp") document.getElementById('temp-stats').classList.add('active'); 299 if(target === "hum") document.getElementById('hum-stats').classList.add('active'); 300 if(target === "pres") document.getElementById('pres-stats').classList.add('active'); 301 if(target === "wind"){ 302 document.getElementById('wind-stats').classList.add('active'); 303 document.getElementById('wind-dir').classList.add('active'); 304 } 305 if(target === "air") document.getElementById('air-stats').classList.add('active'); 306 } 307 }); 308 309 } catch(e){ document.getElementById('error').textContent = 'GreÅ¡ka: '+e.message; } 310} 311 312document.querySelectorAll('button.tab').forEach(b=>{ 313 b.addEventListener('click',()=>{ 314 document.querySelectorAll('button.tab').forEach(x=>x.classList.remove('active')); 315 document.querySelectorAll('.chart-area').forEach(c=>c.classList.remove('active')); 316 document.getElementById('wind-stats').classList.remove('active'); 317 document.getElementById('hum-stats').classList.remove('active'); 318 document.getElementById('temp-stats').classList.remove('active'); 319 document.getElementById('pres-stats').classList.remove('active'); 320 document.getElementById('air-stats').classList.remove('active'); 321 322 b.classList.add('active'); 323 document.getElementById(b.dataset.target).classList.add('active'); 324 325 if(b.dataset.target==="wind"){ 326 document.getElementById('wind-stats').classList.add('active'); 327 document.getElementById('wind-dir').classList.add('active'); 328 } else { 329 document.getElementById('wind-dir').classList.remove('active'); 330 } 331 if(b.dataset.target==="temp") document.getElementById('temp-stats').classList.add('active'); 332 if(b.dataset.target==="hum") document.getElementById('hum-stats').classList.add('active'); 333 if(b.dataset.target==="pres") document.getElementById('pres-stats').classList.add('active'); 334 if(b.dataset.target==="air") document.getElementById('air-stats').classList.add('active'); 335 }); 336}); 337 338loadData(); 339</script>
339 340</body> 341</html>
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.