<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Narzędzia on GW-TARS</title><link>https://dane.tomaszlebioda.com/narzedzia/</link><description>Recent content in Narzędzia on GW-TARS</description><generator>Hugo</generator><language>pl</language><copyright>Tomasz Lebioda · tomlebioda.com</copyright><atom:link href="https://dane.tomaszlebioda.com/narzedzia/index.xml" rel="self" type="application/rss+xml"/><item><title>Geocoder adresów</title><link>https://dane.tomaszlebioda.com/narzedzia/geocode/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://dane.tomaszlebioda.com/narzedzia/geocode/</guid><description>&lt;p&gt;Zamiana adresów na współrzędne geograficzne. Wpisz adres, wklej listę lub wgraj plik CSV/Excel.&lt;/p&gt;
&lt;div class="geocoder-app"&gt;
&lt;div class="geo-input-group"&gt;
&lt;label&gt;Pojedynczy adres&lt;/label&gt;
&lt;input type="text" id="singleAddress" placeholder="np. Warszawa, Złota 44"&gt;
&lt;/div&gt;
&lt;div class="geo-input-group"&gt;
&lt;label&gt;Wiele adresów (jeden na linię)&lt;/label&gt;
&lt;textarea id="multipleAddresses" rows="5" placeholder="Gdańsk, Długa 1&amp;#10;Kraków, Rynek Główny 1&amp;#10;Poznań, Stary Rynek 1"&gt;&lt;/textarea&gt;
&lt;/div&gt;
&lt;div class="geo-input-group"&gt;
&lt;label&gt;Lub wgraj plik CSV/XLS/XLSX&lt;/label&gt;
&lt;input type="file" id="fileInput" accept=".csv,.xls,.xlsx"&gt;
&lt;span class="geo-hint"&gt;Plik z jedną kolumną adresów, bez nagłówka&lt;/span&gt;
&lt;/div&gt;
&lt;div class="geo-buttons"&gt;
&lt;button id="geocodeBtn" class="geo-btn geo-btn-primary"&gt;Geokoduj&lt;/button&gt;
&lt;button id="clearBtn" class="geo-btn geo-btn-danger"&gt;Wyczyść wszystko&lt;/button&gt;
&lt;/div&gt;
&lt;div id="statusMessage" class="geo-status" style="display:none"&gt;&lt;/div&gt;
&lt;div id="downloadButtons" class="geo-downloads" style="display:none"&gt;
&lt;button onclick="downloadCSV()" class="geo-btn geo-btn-small"&gt;Pobierz .csv&lt;/button&gt;
&lt;button onclick="downloadXLS()" class="geo-btn geo-btn-small"&gt;Pobierz .xls&lt;/button&gt;
&lt;button onclick="downloadMD()" class="geo-btn geo-btn-small"&gt;Pobierz .md&lt;/button&gt;
&lt;/div&gt;
&lt;table class="geo-table"&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Adres wejściowy&lt;/th&gt;&lt;th&gt;Latitude&lt;/th&gt;&lt;th&gt;Longitude&lt;/th&gt;&lt;th&gt;Status&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody id="resultsBody"&gt;&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;script src="https://cdn.sheetjs.com/xlsx-0.20.1/package/dist/xlsx.full.min.js"&gt;&lt;/script&gt;
&lt;script&gt;
var singleAddressInput = document.getElementById('singleAddress');
var multipleAddressesInput = document.getElementById('multipleAddresses');
var fileInput = document.getElementById('fileInput');
var geocodeBtn = document.getElementById('geocodeBtn');
var clearBtn = document.getElementById('clearBtn');
var resultsBody = document.getElementById('resultsBody');
var statusMessageDiv = document.getElementById('statusMessage');
function delay(ms) { return new Promise(function(resolve) { setTimeout(resolve, ms); }); }
function showStatus(message, type) {
statusMessageDiv.textContent = message;
statusMessageDiv.className = 'geo-status geo-status-' + (type || 'info');
statusMessageDiv.style.display = 'block';
}
function hideStatus() { statusMessageDiv.style.display = 'none'; }
function addRow(address, lat, lon, status) {
var row = resultsBody.insertRow();
var pLat = parseFloat(lat);
var pLon = parseFloat(lon);
row.insertCell(0).innerText = address;
row.insertCell(1).innerText = !isNaN(pLat) ? pLat.toFixed(6) : '';
row.insertCell(2).innerText = !isNaN(pLon) ? pLon.toFixed(6) : '';
row.insertCell(3).innerText = status;
if (status === 'Sukces') row.classList.add('geo-row-success');
else if (status.indexOf('Błąd') === 0) row.classList.add('geo-row-error');
}
async function geocodeAll(addresses) {
showStatus('Geokodowanie ' + addresses.length + ' adresów...', 'info');
var count = 0;
for (var i = 0; i &lt; addresses.length; i++) {
var addr = addresses[i].trim();
if (!addr) continue;
var randomDelay = Math.floor(Math.random() * 1500) + 1500;
if (i &gt; 0) {
showStatus('Przetwarzam ' + (count + 1) + '/' + addresses.length + '...', 'warning');
await delay(randomDelay);
} else {
showStatus('Przetwarzam 1/' + addresses.length + '.', 'info');
}
try {
var url = 'https://nominatim.openstreetmap.org/search?q=' + encodeURIComponent(addr) + '&amp;format=json&amp;limit=1';
var resp = await fetch(url, { headers: { 'User-Agent': 'GW-TARS Geocoder (tomasz.lebioda@wyborcza.pl)' } });
if (!resp.ok) throw new Error('HTTP ' + resp.status);
var data = await resp.json();
count++;
if (data &amp;&amp; data.length &gt; 0) addRow(addr, data[0].lat, data[0].lon, 'Sukces');
else addRow(addr, '', '', 'Nie znaleziono');
} catch (e) {
addRow(addr, '', '', 'Błąd: ' + e.message);
}
}
showStatus('Zakończono. Przetworzono ' + count + ' adresów.', 'success');
if (count &gt; 0) document.getElementById('downloadButtons').style.display = 'flex';
}
geocodeBtn.addEventListener('click', async function() {
resultsBody.innerHTML = '';
hideStatus();
var addrs = [];
var single = singleAddressInput.value.trim();
if (single) addrs.push(single);
var multi = multipleAddressesInput.value.trim();
if (multi) addrs.push.apply(addrs, multi.split('\n').filter(function(a) { return a.trim(); }));
if (addrs.length === 0 &amp;&amp; fileInput.files.length &gt; 0) {
var reader = new FileReader();
reader.onload = async function(e) {
try {
var wb = XLSX.read(new Uint8Array(e.target.result), { type: 'array' });
var ws = wb.Sheets[wb.SheetNames[0]];
var rows = XLSX.utils.sheet_to_json(ws, { header: 1 });
var fileAddrs = rows.map(function(r) { return r[0]; }).filter(function(a) { return a &amp;&amp; String(a).trim(); }).map(String);
if (fileAddrs.length) await geocodeAll(fileAddrs);
else showStatus('Plik nie zawiera adresów.', 'warning');
} catch (err) { showStatus('Błąd pliku: ' + err.message, 'error'); }
};
reader.readAsArrayBuffer(fileInput.files[0]);
} else if (addrs.length &gt; 0) {
await geocodeAll(addrs);
} else {
showStatus('Wprowadź adresy lub wybierz plik.', 'info');
}
});
clearBtn.addEventListener('click', function() {
resultsBody.innerHTML = '';
singleAddressInput.value = '';
multipleAddressesInput.value = '';
fileInput.value = '';
hideStatus();
document.getElementById('downloadButtons').style.display = 'none';
});
singleAddressInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') { e.preventDefault(); geocodeBtn.click(); }
});
function getTableData() {
var rows = [];
var trs = resultsBody.querySelectorAll('tr');
trs.forEach(function(tr) {
var cells = tr.querySelectorAll('td');
if (cells.length &gt;= 4) {
rows.push({
address: cells[0].innerText,
lat: cells[1].innerText,
lon: cells[2].innerText,
status: cells[3].innerText
});
}
});
return rows;
}
function downloadFile(content, filename, mime) {
var blob = new Blob([content], { type: mime });
var a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = filename;
a.click();
}
function downloadCSV() {
var data = getTableData();
var lines = ['Adres,Latitude,Longitude,Status'];
data.forEach(function(r) {
lines.push('"' + r.address.replace(/"/g, '""') + '",' + r.lat + ',' + r.lon + ',' + r.status);
});
downloadFile(lines.join('\n'), 'geocoded.csv', 'text/csv;charset=utf-8');
}
function downloadXLS() {
var data = getTableData();
var wsData = [['Adres', 'Latitude', 'Longitude', 'Status']];
data.forEach(function(r) { wsData.push([r.address, r.lat ? parseFloat(r.lat) : '', r.lon ? parseFloat(r.lon) : '', r.status]); });
var ws = XLSX.utils.aoa_to_sheet(wsData);
var wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, 'Geocoded');
XLSX.writeFile(wb, 'geocoded.xlsx');
}
function downloadMD() {
var data = getTableData();
var lines = ['| Adres | Latitude | Longitude | Status |', '|-------|----------|-----------|--------|'];
data.forEach(function(r) {
lines.push('| ' + r.address + ' | ' + r.lat + ' | ' + r.lon + ' | ' + r.status + ' |');
});
downloadFile(lines.join('\n'), 'geocoded.md', 'text/markdown;charset=utf-8');
}
&lt;/script&gt;</description></item><item><title>NOTAM Polska</title><link>https://dane.tomaszlebioda.com/narzedzia/notam/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://dane.tomaszlebioda.com/narzedzia/notam/</guid><description>&lt;p&gt;Parser i przeglądarka NOTAMów (Notices to Airmen) dla polskich lotnisk. Wklej tekst lub wgraj plik .txt.&lt;/p&gt;
&lt;div class="notam-app"&gt;
&lt;div class="geo-input-group"&gt;
&lt;label&gt;Wklej NOTAM-y (jeden lub wiele)&lt;/label&gt;
&lt;textarea id="input" rows="5" class="notam-textarea" placeholder="EPBY D0164/25 NOTAMN
Q) EPWW/QMRLC/IV/NBO/A/000/999/5305N01800E005
A) EPBY
B) 2509110800 C) 2509111400
E) RWY 08/26 CLOSED DUE TO MAINTENANCE"&gt;&lt;/textarea&gt;
&lt;/div&gt;
&lt;div class="geo-input-group"&gt;
&lt;label&gt;Lub wczytaj plik .txt&lt;/label&gt;
&lt;input id="file" type="file" accept=".txt,text/plain"&gt;
&lt;/div&gt;
&lt;div class="notam-controls"&gt;
&lt;button id="parse" class="geo-btn geo-btn-primary"&gt;Przetwórz wklejone&lt;/button&gt;
&lt;label&gt;Polska:
&lt;select id="onlyPL" class="notam-input-sm"&gt;
&lt;option value="yes" selected&gt;Tylko EP**&lt;/option&gt;
&lt;option value="no"&gt;Wszystkie&lt;/option&gt;
&lt;/select&gt;
&lt;/label&gt;
&lt;input id="search" class="notam-input-sm" placeholder="Szukaj..." style="width:150px"&gt;
&lt;/div&gt;
&lt;div id="stats" class="notam-stats"&gt;&lt;/div&gt;
&lt;div class="notam-table-wrap"&gt;
&lt;table class="geo-table" id="table"&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Lokalizacja&lt;/th&gt;&lt;th&gt;ID / Typ&lt;/th&gt;&lt;th&gt;Obowiązuje&lt;/th&gt;&lt;th&gt;Interpretacja&lt;/th&gt;&lt;th&gt;Raw&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class="notam-disclaimer"&gt;Do operacji lotniczych korzystaj z oficjalnego AIS (ais.pansa.pl). To narzędzie jest poglądowe.&lt;/div&gt;
&lt;/div&gt;
&lt;script&gt;
var subjectMap = {"QMR":"Runway (RWY)","QMX":"Taxiway (TWY)","QMS":"Stopbar/Lights","QLG":"Lighting","QLR":"Runway lights","QLX":"Taxiway lights","QFU":"Runway-in-use","QFA":"Fire/rescue","QMN":"Apron/Stand","QMD":"Parking/Apron","QSE":"Navaid","QNB":"NDB","QNK":"VOR","QNV":"VOR/DME","QMRLC":"RWY closed","QMRHW":"RWY work in progress","QOBCE":"Obstacle — crane","QWALW":"Air display","QPZCA":"Parachute activity","QRTCA":"Temporary restricted area","QRRCA":"Restricted area active","QXXXX":"Inne"};
var condMap = {"LC":"closed","HW":"work in progress","LA":"available","LD":"deactivated","U":"unserviceable","PA":"partially available","XX":"other"};
function parseQLine(q) {
var m = q.match(/^[QA]\)\s*(.+)$/i);
var line = m ? m[1].trim() : q.trim();
var parts = line.split('/');
var fir = (parts[0]||'').trim(), subjCond = (parts[1]||'').trim();
var subj = subjCond.slice(0,3).toUpperCase(), cond = subjCond.slice(3,5).toUpperCase();
return { fir:fir, subj:subj, cond:cond, subjText:subjectMap[subj]||subjectMap[subj+cond]||'Nieznany', condText:condMap[cond]||'', traffic:(parts[2]||'').trim(), scope:(parts[4]||'').trim(), lower:(parts[5]||'').trim(), upper:(parts[6]||'').trim(), coords:(parts[7]||'').trim() };
}
function yymmddhhmmToISO(s) {
var m = (s||'').match(/^(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/);
if (!m) return null;
var y = Number(m[1]); y += y &gt;= 70 ? 1900 : 2000;
return new Date(Date.UTC(y, Number(m[2])-1, Number(m[3]), Number(m[4]), Number(m[5]))).toISOString().replace('.000Z','Z');
}
function isoNice(iso) { return iso ? new Date(iso).toLocaleString('pl-PL',{year:'numeric',month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'}) : ''; }
function splitNotams(text) {
var lines = text.replace(/\r/g,'').split('\n'), chunks = [], buf = [];
function flush() { if (buf.join('\n').trim().length) chunks.push(buf.join('\n').trim()); buf = []; }
for (var i = 0; i &lt; lines.length; i++) {
var ln = lines[i];
var start = /^\s*[A-Z0-9]{4}\s+[A-Z]?\d{3,4}\/\d{2}\b/.test(ln) || /^\s*Q\)/.test(ln);
if (start &amp;&amp; buf.length) flush();
if (ln.trim()==='' &amp;&amp; buf.length) { buf.push(ln); flush(); continue; }
if (ln.trim()==='' &amp;&amp; !buf.length) continue;
buf.push(ln);
}
flush(); return chunks;
}
function parseOne(text) {
var res = {raw:text.trim(),icao:null,id:null,type:null,q:null,a:null,b:null,c:null,d:null,e:null,qDecoded:null,validFrom:null,validTo:null};
var first = text.split('\n').map(function(s){return s.trim();}).find(function(s){return /^\w{4}\s+.+/.test(s);})||'';
var fm = first.match(/^([A-Z0-9]{4})\s+([A-Z]?\d{3,4}\/\d{2})\s+(NOTAM[NSRC])?/i);
if (fm) { res.icao=fm[1]; res.id=fm[2]; res.type=fm[3]||''; }
function grab(key) { var re = new RegExp('^'+key+'\\)\\s*([\\s\\S]*?)(?=\\n[A-Z]\\)|$)','m'); var m = text.match(re); return m?m[0].trim():null; }
res.q=grab('Q'); res.a=grab('A'); res.b=grab('B'); res.c=grab('C'); res.d=grab('D'); res.e=grab('E');
if (res.q) res.qDecoded = parseQLine(res.q);
var bVal = (res.b||'').replace(/^B\)\s*/,'').trim().split(/\s+/)[0];
var cVal = (res.c||'').replace(/^C\)\s*/,'').trim().split(/\s+/)[0];
res.validFrom = yymmddhhmmToISO(bVal);
res.validTo = yymmddhhmmToISO(cVal);
return res;
}
function isPoland(n) {
var fir = n.qDecoded ? n.qDecoded.fir : '';
var aLine = (n.a||'').replace(/^A\)\s*/,'').trim();
return /^EP/i.test(aLine) || /^EPWW$/i.test(fir);
}
function humanInterp(n) {
var lines = [];
if (n.qDecoded) {
var q = n.qDecoded;
lines.push('&lt;strong&gt;' + q.subjText + (q.condText ? ' — ' + q.condText : '') + '&lt;/strong&gt;');
var ctx = [];
if (q.fir) ctx.push('FIR: ' + q.fir);
if (q.scope) ctx.push('Zasięg: ' + q.scope);
if (q.lower||q.upper) ctx.push('Poziomy: ' + (q.lower||'—') + '/' + (q.upper||'—'));
if (ctx.length) lines.push('&lt;span style="font-size:0.8em;color:#888"&gt;' + ctx.join(' · ') + '&lt;/span&gt;');
}
var a = (n.a||'').replace(/^A\)\s*/,'').trim();
if (a) lines.push('Lokalizacja: ' + a);
var e = (n.e||'').replace(/^E\)\s*/,'').trim();
if (e) lines.push('&lt;div style="margin-top:4px;font-size:0.85em;color:#aaa"&gt;' + e + '&lt;/div&gt;');
return lines.join('&lt;br&gt;');
}
var NOTAMS = [];
function renderTable(list) {
var tbody = document.querySelector('#table tbody');
tbody.innerHTML = '';
var q = (document.getElementById('search').value||'').toLowerCase();
var onlyPL = document.getElementById('onlyPL').value === 'yes';
var filtered = list.filter(function(n) {
if (onlyPL &amp;&amp; !isPoland(n)) return false;
if (!q) return true;
return [n.raw,n.icao,n.id,n.type,n.q||'',n.a||'',n.e||''].join('\n').toLowerCase().indexOf(q) &gt;= 0;
});
filtered.sort(function(a,b) { return (b.validFrom?Date.parse(b.validFrom):0) - (a.validFrom?Date.parse(a.validFrom):0); });
for (var i = 0; i &lt; filtered.length; i++) {
var n = filtered[i];
var tr = document.createElement('tr');
var valid = (n.validFrom?isoNice(n.validFrom):'') + (n.validTo?' → '+isoNice(n.validTo):'');
tr.innerHTML = '&lt;td style="font-family:monospace"&gt;' + (n.icao||'—') + '&lt;div style="font-size:0.7em;color:#555"&gt;' + (n.qDecoded?n.qDecoded.fir:'') + '&lt;/div&gt;&lt;/td&gt;' +
'&lt;td style="font-family:monospace"&gt;' + (n.id||'—') + '&lt;div style="font-size:0.7em;color:#555"&gt;' + (n.type||'') + '&lt;/div&gt;&lt;/td&gt;' +
'&lt;td style="font-size:0.8em"&gt;' + valid + '&lt;/td&gt;' +
'&lt;td&gt;' + humanInterp(n) + '&lt;/td&gt;' +
'&lt;td style="font-size:0.7em;font-family:monospace;white-space:pre-wrap;max-width:250px;overflow:hidden;color:#666"&gt;' + n.raw + '&lt;/td&gt;';
tbody.appendChild(tr);
}
document.getElementById('stats').textContent = filtered.length + ' / ' + list.length + ' NOTAMów';
}
function runParse(text) { NOTAMS = splitNotams(text).map(parseOne); renderTable(NOTAMS); }
document.getElementById('parse').addEventListener('click', function() { runParse(document.getElementById('input').value||''); });
document.getElementById('search').addEventListener('input', function() { renderTable(NOTAMS); });
document.getElementById('onlyPL').addEventListener('change', function() { renderTable(NOTAMS); });
document.getElementById('file').addEventListener('change', async function(e) {
var f = e.target.files[0]; if (!f) return;
document.getElementById('input').value = await f.text();
runParse(document.getElementById('input').value);
});
&lt;/script&gt;</description></item><item><title>Numer wydania GW</title><link>https://dane.tomaszlebioda.com/narzedzia/numer-wydania/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://dane.tomaszlebioda.com/narzedzia/numer-wydania/</guid><description>&lt;p&gt;Kalkulator numerów wydań Gazety Wyborczej na lata 2025–2026. Uwzględnia święta, weekendy i pominięte daty.&lt;/p&gt;
&lt;div class="issue-app"&gt;
&lt;div style="margin-bottom:1rem;display:flex;gap:16px;align-items:center"&gt;
&lt;label style="font-size:0.85rem;color:var(--secondary)"&gt;&lt;input type="checkbox" id="hidePast" checked&gt; Ukryj przeszłe daty&lt;/label&gt;
&lt;span id="tomorrowInfo" style="font-size:0.85rem;color:#92d050;font-weight:600"&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class="notam-table-wrap"&gt;
&lt;table class="geo-table" id="issueTable"&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Data&lt;/th&gt;&lt;th&gt;Dzień&lt;/th&gt;&lt;th&gt;Święto&lt;/th&gt;&lt;th&gt;Nr wydania&lt;/th&gt;&lt;th&gt;Imieniny&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody id="issueBody"&gt;&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div style="margin-top:1rem;font-size:0.75rem;color:#555"&gt;
Pominięte daty (skip_dates.json): edytowalne w repozytorium.&lt;br&gt;
Algorytm: +1 w dni robocze i soboty, 0 w niedziele/święta/skip dates, reset 1 stycznia.
&lt;/div&gt;
&lt;/div&gt;
&lt;script src="https://dane.tomaszlebioda.com/narzedzia/namedays.js"&gt;&lt;/script&gt;
&lt;script&gt;
var HOLIDAYS = {
"01-01":"Nowy Rok","01-06":"Święto Trzech Króli","05-01":"Święto Pracy",
"05-03":"Święto Konstytucji 3 Maja","08-15":"Wniebowzięcie NMP",
"11-01":"Wszystkich Świętych","11-11":"Narodowe Święto Niepodległości",
"12-25":"Boże Narodzenie","12-26":"Boże Narodzenie (2. dzień)"
};
var DAYS_PL = ["niedziela","poniedziałek","wtorek","środa","czwartek","piątek","sobota"];
// Easter calculation (anonymous Gregorian algorithm)
function easter(year) {
var a=year%19, b=Math.floor(year/100), c=year%100;
var d=Math.floor(b/4), e=b%4, f=Math.floor((b+8)/25);
var g=Math.floor((b-f+1)/3), h=(19*a+b-d-g+15)%30;
var i=Math.floor(c/4), k=c%4, l=(32+2*e+2*i-h-k)%7;
var m=Math.floor((a+11*h+22*l)/451);
var month=Math.floor((h+l-7*m+114)/31), day=((h+l-7*m+114)%31)+1;
return new Date(year, month-1, day);
}
function dateToMMDD(d) { return pad(d.getMonth()+1) + '-' + pad(d.getDate()); }
function getMovableHolidays(year) {
var e = easter(year);
var result = {};
// Easter Sunday
result[dateToMMDD(e)] = "Wielkanoc";
// Easter Monday
var em = new Date(e); em.setDate(em.getDate()+1); result[dateToMMDD(em)] = "Poniedziałek Wielkanocny";
// Corpus Christi (60 days after Easter)
var cc = new Date(e); cc.setDate(cc.getDate()+60); result[dateToMMDD(cc)] = "Boże Ciało";
// Whit Sunday (49 days after Easter)
var ws = new Date(e); ws.setDate(ws.getDate()+49); result[dateToMMDD(ws)] = "Zesłanie Ducha Świętego";
return result;
}
var skipDates = [];
function isHoliday(date) {
var mmdd = String(date.getMonth()+1).padStart(2,'0') + '-' + String(date.getDate()).padStart(2,'0');
var movable = getMovableHolidays(date.getFullYear());
return HOLIDAYS[mmdd] || movable[mmdd] || null;
}
function isSkipped(dateStr) { return skipDates.indexOf(dateStr) &gt;= 0; }
function pad(n) { return String(n).padStart(2,'0'); }
function buildTable() {
var tbody = document.getElementById('issueBody');
tbody.innerHTML = '';
var hidePast = document.getElementById('hidePast').checked;
var today = new Date(); today.setHours(0,0,0,0);
var tomorrow = new Date(today); tomorrow.setDate(tomorrow.getDate()+1);
var todayStr = today.getFullYear() + '-' + pad(today.getMonth()+1) + '-' + pad(today.getDate());
var tomorrowStr = tomorrow.getFullYear() + '-' + pad(tomorrow.getMonth()+1) + '-' + pad(tomorrow.getDate());
var todayNum = null;
var nextIssueNum = null;
var nextIssueDateStr = null;
for (var year = 2025; year &lt;= 2026; year++) {
var issueNum = 0;
var start = new Date(year, 0, 1);
var end = new Date(year, 11, 31);
for (var d = new Date(start); d &lt;= end; d.setDate(d.getDate()+1)) {
var dateStr = d.getFullYear() + '-' + pad(d.getMonth()+1) + '-' + pad(d.getDate());
var day = d.getDay(); // 0=Sunday
var holiday = isHoliday(d);
var skipped = isSkipped(dateStr);
// Issue number logic
if (d.getMonth() === 0 &amp;&amp; d.getDate() === 1) issueNum = 0;
if (holiday || skipped) {
// no increment
} else if (day === 0) {
// Sunday — no increment
} else {
issueNum++;
}
var isIssueDay = !(holiday || skipped || day === 0);
if (dateStr === todayStr &amp;&amp; isIssueDay) todayNum = issueNum;
if (dateStr &gt; todayStr &amp;&amp; isIssueDay &amp;&amp; nextIssueNum === null) {
nextIssueNum = issueNum;
nextIssueDateStr = dateStr;
}
if (hidePast &amp;&amp; dateStr &lt; todayStr) continue;
var isNextIssue = dateStr === nextIssueDateStr;
var isWeekend = day === 0 || day === 6;
var isOff = holiday || skipped;
var rowClass = '';
if (isNextIssue) rowClass = 'issue-tomorrow';
else if (isOff || day === 0) rowClass = 'issue-off';
else if (isWeekend) rowClass = 'issue-weekend';
var nameday = NAMEDAYS[dateStr] || '';
var tr = document.createElement('tr');
tr.className = rowClass;
tr.innerHTML = '&lt;td style="font-family:monospace;white-space:nowrap"&gt;' + dateStr + '&lt;/td&gt;' +
'&lt;td&gt;' + DAYS_PL[day] + '&lt;/td&gt;' +
'&lt;td style="font-size:0.8rem;color:#f0c800"&gt;' + (holiday || (skipped ? 'pominięte' : '')) + '&lt;/td&gt;' +
'&lt;td style="font-weight:700;text-align:center"&gt;' + (issueNum || '') + '&lt;/td&gt;' +
'&lt;td style="font-size:0.8rem;color:#888"&gt;' + nameday + '&lt;/td&gt;';
tbody.appendChild(tr);
}
}
if (nextIssueNum !== null) {
document.getElementById('tomorrowInfo').textContent =
'Kolejny numer wydania: ' + nextIssueNum + ' (' + nextIssueDateStr + ')';
}
}
document.getElementById('hidePast').addEventListener('change', buildTable);
// Load skip dates then build
fetch('/csv/skip_dates.json')
.then(function(r) { return r.json(); })
.then(function(data) { skipDates = data; })
.catch(function() {})
.finally(buildTable);
&lt;/script&gt;</description></item><item><title>Ukraine Frontline — Polygon Fetcher</title><link>https://dane.tomaszlebioda.com/narzedzia/ukraine-frontline/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://dane.tomaszlebioda.com/narzedzia/ukraine-frontline/</guid><description>&lt;p&gt;Pobieranie plików GeoJSON z danymi o linii frontu w Ukrainie z projektu &lt;a href="https://github.com/cyterat/deepstate-map-data"&gt;DeepState Map&lt;/a&gt;. Dane dostępne od 8 lipca 2024.&lt;/p&gt;
&lt;div class="ukraine-app"&gt;
&lt;div class="geo-buttons" style="margin-bottom:1.5rem"&gt;
&lt;button id="downloadLatest" class="geo-btn geo-btn-primary"&gt;Pobierz najnowszy poligon (dziś)&lt;/button&gt;
&lt;a href="https://github.com/cyterat/deepstate-map-data" target="_blank" class="geo-btn geo-btn-small"&gt;Repozytorium GitHub&lt;/a&gt;
&lt;/div&gt;
&lt;div class="geo-input-group"&gt;
&lt;label&gt;Wybierz datę historyczną&lt;/label&gt;
&lt;div style="display:flex;gap:10px;align-items:center"&gt;
&lt;input type="date" id="datePicker" min="2024-07-08" class="notam-input-sm" style="width:160px"&gt;
&lt;button id="downloadDate" class="geo-btn geo-btn-primary"&gt;Pobierz&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id="downloadUrl" class="ukraine-url" style="display:none"&gt;&lt;/div&gt;
&lt;div id="statusMsg" class="geo-status" style="display:none"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;script&gt;
var BASE = 'https://raw.githubusercontent.com/cyterat/deepstate-map-data/main/data';
function pad(n) { return String(n).padStart(2, '0'); }
function buildUrl(date) {
var y = date.getFullYear();
var m = pad(date.getMonth() + 1);
var d = pad(date.getDate());
return BASE + '/deepstatemap_data_' + y + m + d + '.geojson';
}
function showUrl(url) {
var el = document.getElementById('downloadUrl');
el.innerHTML = '&lt;span style="font-size:0.8rem;color:#888"&gt;URL: &lt;/span&gt;&lt;a href="' + url + '" target="_blank" style="font-size:0.8rem;word-break:break-all"&gt;' + url + '&lt;/a&gt;';
el.style.display = 'block';
}
function downloadFile(url, filename) {
document.getElementById('statusMsg').style.display = 'none';
showUrl(url);
fetch(url)
.then(function(r) {
if (!r.ok) throw new Error('HTTP ' + r.status + ' — plik może nie istnieć dla tej daty');
return r.blob();
})
.then(function(blob) {
var a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = filename;
a.click();
var msg = document.getElementById('statusMsg');
msg.textContent = 'Pobrano: ' + filename;
msg.className = 'geo-status geo-status-success';
msg.style.display = 'block';
})
.catch(function(e) {
var msg = document.getElementById('statusMsg');
msg.textContent = 'Błąd: ' + e.message;
msg.className = 'geo-status geo-status-error';
msg.style.display = 'block';
});
}
// Set date picker max to today
var today = new Date();
var picker = document.getElementById('datePicker');
picker.max = today.toISOString().slice(0, 10);
picker.value = today.toISOString().slice(0, 10);
document.getElementById('downloadLatest').addEventListener('click', function() {
var url = buildUrl(today);
var y = today.getFullYear(), m = pad(today.getMonth()+1), d = pad(today.getDate());
downloadFile(url, 'deepstatemap_data_' + y + m + d + '.geojson');
});
document.getElementById('downloadDate').addEventListener('click', function() {
var val = picker.value;
if (!val) return;
var parts = val.split('-');
var date = new Date(Number(parts[0]), Number(parts[1])-1, Number(parts[2]));
var y = date.getFullYear(), m = pad(date.getMonth()+1), d = pad(date.getDate());
var url = buildUrl(date);
downloadFile(url, 'deepstatemap_data_' + y + m + d + '.geojson');
});
&lt;/script&gt;</description></item></channel></rss>