4bdfb673cc
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 11m2s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m56s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 34s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 23s
build / build (push) Successful in 1m40s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 11m12s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m52s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 32s
Testy / Kontrola składni wszystkich warstw (push) Successful in 19s
„Logika dwóch lokalizacji": dotąd offset GMT był ręcznym polem (PRE-19) — trzeba
było go znać i samemu pamiętać o czasie letnim. Teraz liczymy go z lokalizacji.
Sedno wymagania: strefę ustalamy RAZ i trzymamy jako stałą liczbę, żeby drobna
zmiana współrzędnych nie przerzuciła DST i nie „przeskoczyła" Ascendenta na
sąsiedni znak. „Większa miejscowość z bazy" okazuje się zbędna — strefa IANA jest
i tak regionalna, więc wioska daje tę samą strefę co pobliskie miasto.
Jak:
- `timezone.py`: współrzędne → strefa IANA (tzfpy, OFFLINE — bez sieci), a z niej
offset DLA DATY URODZENIA. `zoneinfo`/`tzdata` znają reguły historyczne i DST:
Kraków 1984 to +1h zimą, +2h latem; Katmandu +5:45. Degraduje się do None (brak
biblioteki / punkt bez strefy / zła data) — wtedy zostaje ręczny offset.
- Endpoint `GET /timezone?lat&lon&date&time` → {tz, offset, dst, label}. 404, gdy
nie da się ustalić.
- `geo.js`: po wyborze miejsca (mapa / wyszukiwarka / „Tu i teraz") oraz przy
zmianie DATY (bo DST zależy od pory roku) pobiera offset i wypełnia pole
tz_offset, pokazując wykrytą strefę („Wykryto: Europe/Warsaw · +2:00 (czas
letni)"). Pole zostaje edytowalne. Na wejściu podpowiada tylko gdy offset
wygląda na nieustawiony — nie nadpisuje wartości ręcznie wpisanej i wysłanej.
Zależności (lekkie, offline): tzfpy (wheel Rust) + tzdata (dla zoneinfo w slim-obrazie).
Weryfikacja: żywy serwer — Kraków 1984-06 → +2:00 (czas letni), 1984-01 → +1:00,
Katmandu → +5:45. Testy: +12 strefa (moduł + endpoint), +5 JS. Prezentacja 187.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
183 lines
8.3 KiB
JavaScript
183 lines
8.3 KiB
JavaScript
// 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();
|
|
});
|
|
})();
|