// Wyszukiwarka lokalizacji + mapa (OpenStreetMap / Leaflet, bez klucza API). // Wpina się do formularza z polami input[name=lat] / input[name=lon]: // - wpisanie nazwy/adresu/POI -> /geocode -> lista trafień -> klik ustawia lat/lon, // - klik na mapie lub przeciągnięcie pineski -> ustawia lat/lon + /reverse pokazuje nazwę, // - „Tu i teraz" (now.js) emituje event 'astrololo:coords' -> mapa się synchronizuje. // Bez internetu wyszukiwarka/kafelki nie działają, ale ręczne lat/lon dalej tak. (function () { function ready(fn) { if (document.readyState !== 'loading') fn(); else document.addEventListener('DOMContentLoaded', fn); } ready(function () { var picker = document.getElementById('geoPicker'); if (!picker || typeof L === 'undefined') return; // brak Leafleta = po cichu pas var latEl = document.querySelector('input[name=lat]'); var lonEl = document.querySelector('input[name=lon]'); if (!latEl || !lonEl) return; var searchEl = document.getElementById('geoSearch'); var searchBtn = document.getElementById('geoSearchBtn'); var mapToggle = document.getElementById('geoMapToggle'); var resultsEl = document.getElementById('geoResults'); var mapEl = document.getElementById('map'); // ---- strefa czasowa z lokalizacji (PRE-03) ---- // Offset GMT liczymy ze współrzędnych + daty (DST-świadomie), żeby użytkownik // nie musiał go znać ani pamiętać o czasie letnim. Podpowiedź trafia do pola // tz_offset (PRE-19), które zostaje edytowalne — to pomoc, nie kaganiec. var tzEl = document.querySelector('input[name=tz_offset]'); var dateEl = document.querySelector('input[name=date]'); var timeEl = document.querySelector('input[name=time]'); var tzNote = null; if (tzEl && tzEl.parentNode) { tzNote = document.createElement('span'); tzNote.className = 'muted small'; tzNote.id = 'tzNote'; tzEl.parentNode.appendChild(tzNote); } var num = function (el) { var v = parseFloat(el.value); return isNaN(v) ? 0 : v; }; var curLat = function () { return num(latEl); }; var curLon = function () { return num(lonEl); }; var pin = L.divIcon({ className: 'geo-pin', html: '📍', iconSize: [24, 24], iconAnchor: [12, 24] }); var map = L.map(mapEl).setView([curLat(), curLon()], 12); L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© OpenStreetMap contributors' }).addTo(map); var marker = L.marker([curLat(), curLon()], { draggable: true, icon: pin }).addTo(map); // Kontener mógł być renderowany zanim ustaliła się jego szerokość — poprawka. setTimeout(function () { map.invalidateSize(); }, 60); // Ustawienie `.value` z kodu NIE wywołuje zdarzeń, więc synchronizacja // między zakładkami (formsync.js) przegapiłaby zmiany z mapy. Zgłaszamy je // jawnie przez `change`. function setVal(el, value) { if (!el) return; el.value = value; el.dispatchEvent(new Event('change', { bubbles: true })); } function setCoords(lat, lon, fromMap) { setVal(latEl, lat.toFixed(4)); setVal(lonEl, lon.toFixed(4)); marker.setLatLng([lat, lon]); if (!fromMap) map.setView([lat, lon], Math.max(map.getZoom(), 12)); if (fromMap) reverseName(lat, lon); resolveTz(); // nowe współrzędne → odśwież strefę/offset } // Współrzędne + data → offset GMT (DST-świadomy). Wypełnia tz_offset i pokazuje, // jaką strefę wykryto. Bez pola/daty/biblioteki po cichu pasuje — zostaje // ręczny offset. Offset trzymamy potem jako stałą liczbę: to on decyduje przy // przeliczaniu, więc dokładne współrzędne nie przerzucą już czasu letniego. function resolveTz() { if (!tzEl || !dateEl || !dateEl.value) return; var lat = curLat(), lon = curLon(); if (!lat && !lon) return; // 0,0 = brak lokalizacji var url = '/timezone?lat=' + lat + '&lon=' + lon + '&date=' + encodeURIComponent(dateEl.value) + '&time=' + encodeURIComponent((timeEl && timeEl.value) || '12:00'); fetch(url) .then(function (r) { return r.ok ? r.json() : null; }) .then(function (d) { if (!d) { if (tzNote) tzNote.textContent = ''; return; } setVal(tzEl, d.offset); // change → formsync podłapie if (tzNote) tzNote.textContent = 'Wykryto: ' + d.label; }) .catch(function () {}); } function reverseName(lat, lon) { // Czyścimy pole OD RAZU. Dopóki nie znamy nazwy nowego punktu, stara jest // po prostu nieprawdziwa — pokazywałaby inne miejsce niż współrzędne obok. // Dotyczy też sytuacji, gdy /reverse nie odpowie (brak sieci): lepiej puste // pole i poprawne współrzędne niż nazwa, która kłamie. if (searchEl) setVal(searchEl, ''); fetch('/reverse?lat=' + lat + '&lon=' + lon) .then(function (r) { return r.ok ? r.json() : null; }) .then(function (d) { if (d && d.name && searchEl) setVal(searchEl, d.name); }) .catch(function () {}); } marker.on('dragend', function () { var p = marker.getLatLng(); setCoords(p.lat, p.lng, true); }); map.on('click', function (e) { setCoords(e.latlng.lat, e.latlng.lng, true); }); // ---- wyszukiwanie po nazwie ---- function renderResults(items) { resultsEl.innerHTML = ''; if (!items.length) { resultsEl.hidden = true; return; } items.forEach(function (it) { var li = document.createElement('li'); li.textContent = it.name; li.addEventListener('click', function () { setCoords(it.lat, it.lon, false); setVal(searchEl, it.name); resultsEl.hidden = true; }); resultsEl.appendChild(li); }); resultsEl.hidden = false; } function doSearch() { var q = searchEl.value.trim(); if (q.length < 3) { resultsEl.hidden = true; return; } fetch('/geocode?q=' + encodeURIComponent(q)) .then(function (r) { return r.ok ? r.json() : { results: [] }; }) .then(function (d) { renderResults(d.results || []); }) .catch(function () { renderResults([]); }); } var timer = null; searchBtn.addEventListener('click', doSearch); searchEl.addEventListener('keydown', function (e) { if (e.key === 'Enter') { e.preventDefault(); doSearch(); } // nie wysyłaj formularza }); searchEl.addEventListener('input', function () { clearTimeout(timer); timer = setTimeout(doSearch, 500); // debounce (limit Nominatim) }); // ---- zwijanie/rozwijanie mapy ---- mapToggle.addEventListener('click', function () { var hidden = mapEl.hasAttribute('hidden'); if (hidden) { mapEl.removeAttribute('hidden'); mapToggle.textContent = 'Ukryj mapę'; setTimeout(function () { map.invalidateSize(); map.setView([curLat(), curLon()]); }, 60); } else { mapEl.setAttribute('hidden', ''); mapToggle.textContent = 'Pokaż mapę'; } }); // ---- synchronizacja z „Tu i teraz" ---- // Poza przesunięciem pineski MUSIMY odświeżyć pole tekstowe: po „Tu i teraz" // zostawała w nim stara, ręcznie wpisana nazwa, więc formularz pokazywał jedno // miejsce, a współrzędne wskazywały już inne. reverseName pyta o nazwę punktu, // na który faktycznie ustawiła się mapa. document.addEventListener('astrololo:coords', function () { marker.setLatLng([curLat(), curLon()]); map.setView([curLat(), curLon()], Math.max(map.getZoom(), 12)); reverseName(curLat(), curLon()); resolveTz(); // „Tu i teraz" → nowe miejsce → nowa strefa }); // Zmiana daty przelicza offset — czas letni zależy od daty (inny w czerwcu // niż w styczniu), więc bez tego offset zostałby z poprzedniej pory roku. if (dateEl) dateEl.addEventListener('change', resolveTz); // Na wejściu podpowiadamy strefę TYLKO gdy offset wygląda na nieustawiony // (0 lub pusto) — żeby nie nadpisać wartości, którą użytkownik wpisał ręcznie // i wysłał. Przy każdej późniejszej zmianie miejsca/daty już aktualizujemy. if (tzEl && (tzEl.value === '' || parseFloat(tzEl.value) === 0)) resolveTz(); }); })();