Compare commits

..

11 Commits

Author SHA1 Message Date
gitea d4a37aca52 feat(presentation): wyszukiwarka lokalizacji + mapa (OSM/Leaflet)
QoL 2/2: pole „Szukaj miejsca" (nazwa/adres/POI) + interaktywna mapa na
formularzach horoskopu / interpretacji / kalendarza. Bez klucza API.

- geocode.py: proxy OSM/Nominatim PO STRONIE SERWERA (poprawny User-Agent,
  throttling ~1 req/s, cache TTL 1h) — endpointy /geocode i /reverse.
  Wolanie z serwera, nie z przegladarki: latwiej trzymac polityke Nominatim
  i dziala niezaleznie od secure-context (http://<ip>).
- _location_picker.html: wspolny partial (search + wyniki + mapa + atrybucja),
  wpiety includem do 3 formularzy z polami lat/lon.
- geo.js: Leaflet — wyszukiwanie (debounce), klik na wynik ustawia lat/lon i
  centruje mape, klik/drag pineski ustawia wspolrzedne + /reverse pokazuje
  nazwe; sync z „Tu i teraz" (now.js emituje event astrololo:coords).
- Leaflet 1.9.4 vendorowany lokalnie (static/vendor/leaflet, BSD-2-Clause,
  permisywny) — niezaleznosc od CDN; kafelki mapy z OSM. Marker jako divIcon
  (bez plikow PNG).
- styles.css: style pod ciemny motyw.

Zweryfikowane w przegladarce: domyslny widok (pineska na Szpitalu Barlickiego),
wyszukanie „Wawel Krakow" -> lista -> klik ustawia 50.0547/19.9361 i przesuwa
mape, klik w mape ustawia wspolrzedne + reverse wypelnia nazwe. Zero bledow
w konsoli. /geocode i /reverse zwracaja szpital; cache dziala.

Uwaga wdrozeniowa: pod prezentacji potrzebuje egressu do
nominatim.openstreetmap.org; przegladarki — do tile.openstreetmap.org.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 03:26:03 +02:00
gitea e39c31618d feat(presentation): domyslna lokalizacja = Szpital Barlickiego, Lodz
QoL: formularze (horoskop / interpretacje / kalendarz) maja wstepnie
wpisana lokalizacje urodzenia wlasciciela — Szpital Barlickiego w Lodzi
(51.7739N, 19.4829E; potwierdzone reverse-geokodowaniem OSM: Kopcinskiego
22/28). Nie trzeba jej wpisywac za kazdym razem.

- config.py: jedno zrodlo prawdy (DEFAULT_LAT/LON/LABEL, nadpisywalne ENV)
  + helper default_form().
- main.py: GET wstrzykuje default_form() + location_label do 3 formularzy
  z polami lokalizacji (significators pominiete — nie ma tam lat/lon).
- szablony: dyskretna podpowiedz z nazwa lokalizacji, widoczna tylko na
  czystym formularzu (po POST znika, wygrywa wpisana wartosc).
- „Tu i teraz" nadal nadpisuje domyslne wspolrzedne geolokalizacja.

Zweryfikowane TestClientem: 3 strony renderuja 51.7739/19.4829 + etykiete;
POST z innymi wspolrzednymi je zachowuje i chowa podpowiedz.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 03:12:59 +02:00
gitea c75f8377bf kick image updater
build / build (push) Successful in 49s
2026-07-20 19:52:21 +02:00
gitea 5234e86c95 ttt
build / build (push) Successful in 1m18s
2026-07-20 14:50:11 +02:00
gitea 0ac8df250c test image updatera
build / build (push) Successful in 51s
2026-07-19 23:20:39 +02:00
gitea 9323803cb4 tt
build / build (push) Successful in 5m31s
2026-07-18 21:57:33 +02:00
gitea 678c3bad76 test 2 2026-07-18 18:11:42 +02:00
gitea 88a1ecd3c4 test akcji 2026-07-18 18:04:16 +02:00
gitea b28c612bf7 CI: jawne pobranie jądra efemeryd + brak silnika = błąd zamiast pomijania
Pierwszy run CI dał 59 passed / 20 skipped zamiast 78/1: auto-pobranie jądra
przez Skyfield nie powiodło się na runnerze, więc testy referencyjne (walidacja
względem astro.com) cicho znikały.

- workflow: jawne pobranie de421.bsp curlem z ssd.jpl.nasa.gov (naif zwraca 404),
  z retry; EPHEMERIS_DIR wskazany explicite; pytest z -rs (widoczne powody skipów).
- conftest: gdy CI=true, niedostępny silnik kończy się pytest.fail zamiast skip —
  żeby utrata pokrycia nigdy więcej nie przeszła niezauważona. Lokalnie (dev bez
  pobranego jądra) nadal łagodny skip.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 14:47:50 +02:00
gitea 2bda4e311d CI: uruchamianie testów przy każdym pushu (GitHub Actions)
.github/workflows/tests.yml:
- job "Testy warstwy logicznej": Python 3.12 (jak w obrazach Dockera),
  instalacja requirements-dev, pytest na services/logic/tests. Cache pip oraz
  cache jądra efemeryd JPL (de421.bsp ~17 MB), by nie pobierać go co run.
- job "Kontrola składni": compileall na wszystkich warstwach (bez instalowania
  zależności, w tym AGPL-owego silnika swisseph).
- Triggery: każdy push (dowolna gałąź) + pull requesty do master.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 14:02:37 +02:00
gitea 5d0d86cb90 Merge pull request #22 from migatu/feat/firdaria-log11
Firdaria (LOG-11) — perska technika time-lord
2026-07-17 22:50:39 +02:00
17 changed files with 1043 additions and 6 deletions
+2 -2
View File
@@ -1,7 +1,7 @@
name: build
on:
push:
branches: [main]
branches: [master]
jobs:
build:
runs-on: ubuntu-latest
@@ -16,4 +16,4 @@ jobs:
docker build -t gitea.czernobog.pl/gitea/astrololo-$SVC:$TAG ./services/$SVC
docker push gitea.czernobog.pl/gitea/astrololo-$SVC:$TAG
done
echo "Tag: $TAG"
echo "Tag: $TAG"
+65
View File
@@ -0,0 +1,65 @@
name: Testy
# Odpala się przy każdym pushu (dowolna gałąź) oraz dla pull requestów do master.
on:
push:
pull_request:
branches: [master]
jobs:
logic-tests:
name: Testy warstwy logicznej (silnik)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Python 3.12 (jak w obrazach Dockera)
uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
cache-dependency-path: services/logic/requirements-dev.txt
# Jądro efemeryd JPL (de421.bsp, ~17 MB). Cache'ujemy je między runami.
- name: Cache jądra efemeryd
uses: actions/cache@v4
with:
path: services/logic/.ephemeris
key: ephemeris-de421
# Pobieramy jawnie (a nie licząc na auto-pobranie przez Skyfield), żeby
# brak jądra był twardym błędem, a nie cichym pomijaniem testów.
- name: Pobierz jądro efemeryd (gdy brak w cache)
run: |
mkdir -p services/logic/.ephemeris
if [ ! -s services/logic/.ephemeris/de421.bsp ]; then
curl -fSL --retry 3 --max-time 300 \
-o services/logic/.ephemeris/de421.bsp \
https://ssd.jpl.nasa.gov/ftp/eph/planets/bsp/de421.bsp
fi
ls -lh services/logic/.ephemeris/de421.bsp
- name: Instalacja zależności
run: pip install -r services/logic/requirements-dev.txt
# CI=true (ustawiane przez GitHub) sprawia, że brak silnika = błąd,
# a nie pominięcie — patrz tests/conftest.py.
- name: Testy (pytest)
working-directory: services/logic
env:
PYTHONPATH: .
EPHEMERIS_DIR: ${{ github.workspace }}/services/logic/.ephemeris
run: pytest tests -q -rs
compile-all:
name: Kontrola składni wszystkich warstw
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
# Sam kompilator — bez instalowania zależności warstw (w tym AGPL-owego
# silnika swisseph, który nie wchodzi do produktu).
- name: py_compile
run: python -m compileall -q services
+1
View File
@@ -73,3 +73,4 @@ export DATA_PROVIDER=sql # przełącz całą warstwę
`SqlDataProvider` realizuje ten sam kontrakt `/search`, więc **warstwa logiczna i
prezentacji nie zmieniają ani jednej linii**. Odwrócony indeks z L4 (SQLite) jest
już pomostem — rozbudowa o wszystkie kolumny = docelowa baza.
# updater test Mon 20 Jul 2026 19:52:08 CEST
+5
View File
@@ -7,6 +7,7 @@ służą jako wyrocznia (LOG-25).
from __future__ import annotations
import datetime as dt
import os
import pytest
@@ -28,4 +29,8 @@ def own_engine():
try:
return SkyfieldEngine()
except Exception as e: # brak efemeryd / brak sieci
# Lokalnie pomijamy (dev bez pobranego jądra), ale w CI to musi być błąd —
# inaczej testy referencyjne (walidacja względem astro.com) cicho znikają.
if os.getenv("CI"):
pytest.fail(f"CI: silnik efemeryd niedostępny — testy referencyjne muszą działać: {e}")
pytest.skip(f"Nie można wczytać efemeryd: {e}")
+14
View File
@@ -15,3 +15,17 @@ class Settings:
settings = Settings()
# Domyślna (wklepana) lokalizacja formularzy — miejsce urodzenia właściciela:
# Szpital Barlickiego w Łodzi, 51°46'26.1"N 19°28'58.6"E (potwierdzone reverse-
# geokodowaniem OSM: Kopcińskiego 22/28). QoL — nie trzeba wpisywać jej za każdym
# razem. Przycisk „Tu i teraz" nadal ją nadpisuje geolokalizacją przeglądarki.
# Można nadpisać zmiennymi środowiskowymi (np. inny współpracownik).
DEFAULT_LAT: float = float(os.getenv("DEFAULT_LAT", "51.7739"))
DEFAULT_LON: float = float(os.getenv("DEFAULT_LON", "19.4829"))
DEFAULT_LOCATION_LABEL: str = os.getenv("DEFAULT_LOCATION_LABEL", "Szpital Barlickiego, Łódź")
def default_form() -> dict:
"""Wstępnie wypełnione pola lokalizacji na czystym formularzu (GET)."""
return {"lat": DEFAULT_LAT, "lon": DEFAULT_LON}
+95
View File
@@ -0,0 +1,95 @@
"""Proxy geokodera (OpenStreetMap / Nominatim) dla wyszukiwarki lokalizacji w UI.
QoL: pozwala wpisać nazwę / adres / POI i dostać współrzędne (oraz odwrotnie —
z punktu na mapie nazwę miejsca). Wołamy Nominatim **po stronie serwera**, nie
z przeglądarki, bo:
- polityka Nominatim wymaga sensownego User-Agent i max ~1 zapytanie/s — łatwiej
to wyegzekwować i cache'ować centralnie niż z wielu klientów,
- działa niezależnie od „secure context" przeglądarki (na http://<ip> też).
Atrybucja „© OpenStreetMap contributors" jest pokazywana w UI (mapa + wyniki).
Zależność sieciowa jest OPCJONALNA: brak internetu = wyszukiwarka zwraca błąd,
ale ręczne wpisanie lat/lon oraz mapa działają dalej.
"""
from __future__ import annotations
import threading
import time
from typing import Any
import httpx
NOMINATIM = "https://nominatim.openstreetmap.org"
# Nominatim wymaga identyfikującego User-Agent (inaczej blokuje). Kontakt = właściciel.
USER_AGENT = "astrololo/1.0 (self-hosted astrology tool; +mtuszowski@gmail.com)"
_MIN_INTERVAL = 1.1 # s — polityka Nominatim: max ~1 zapytanie/s
_CACHE_TTL = 3600.0 # s — powtórki tego samego zapytania z pamięci przez godzinę
_TIMEOUT = 6.0
_lock = threading.Lock() # serializuje wołania (throttling globalny w procesie)
_last_call = 0.0
_cache: dict[str, tuple[float, Any]] = {}
def _throttle() -> None:
global _last_call
wait = _MIN_INTERVAL - (time.monotonic() - _last_call)
if wait > 0:
time.sleep(wait)
_last_call = time.monotonic()
def _cached(key: str) -> Any | None:
hit = _cache.get(key)
if hit and (time.monotonic() - hit[0]) < _CACHE_TTL:
return hit[1]
return None
def _get(path: str, params: dict) -> Any:
"""Wołanie Nominatim z throttlingiem; zakłada trzymany `_lock`."""
_throttle()
r = httpx.get(
f"{NOMINATIM}{path}",
params=params,
headers={"User-Agent": USER_AGENT, "Accept-Language": "pl"},
timeout=_TIMEOUT,
)
r.raise_for_status()
return r.json()
def search(query: str, limit: int = 5) -> list[dict]:
"""Nazwa/adres/POI → lista kandydatów [{name, lat, lon}] (najlepsze pierwsze)."""
query = (query or "").strip()
if not query:
return []
key = f"s:{limit}:{query.lower()}"
with _lock:
cached = _cached(key)
if cached is not None:
return cached
data = _get("/search", {"q": query, "format": "jsonv2",
"limit": limit, "addressdetails": 0})
out = [
{"name": item.get("display_name", ""),
"lat": round(float(item["lat"]), 6),
"lon": round(float(item["lon"]), 6)}
for item in data if "lat" in item and "lon" in item
]
_cache[key] = (time.monotonic(), out)
return out
def reverse(lat: float, lon: float) -> dict:
"""Punkt (lat, lon) → nazwa miejsca {name, lat, lon}."""
key = f"r:{round(lat, 5)}:{round(lon, 5)}"
with _lock:
cached = _cached(key)
if cached is not None:
return cached
data = _get("/reverse", {"lat": lat, "lon": lon, "format": "jsonv2"})
out = {"name": data.get("display_name", ""),
"lat": round(lat, 6), "lon": round(lon, 6)}
_cache[key] = (time.monotonic(), out)
return out
+34 -4
View File
@@ -12,12 +12,14 @@ from __future__ import annotations
from datetime import datetime, timedelta, timezone
import httpx
from fastapi import FastAPI, Form, Request
from fastapi import FastAPI, Form, HTTPException, Query, Request
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from app import geocode
from app.clients.logic_client import LogicClient
from app.config import DEFAULT_LOCATION_LABEL, default_form
app = FastAPI(title="astrololo · warstwa prezentacji")
app.mount("/static", StaticFiles(directory="app/static"), name="static")
@@ -48,7 +50,10 @@ def _logic_error(e: Exception) -> str:
# ---------------- Horoskop: pozycje (strona główna) ----------------
@app.get("/", response_class=HTMLResponse)
def chart_form(request: Request):
return templates.TemplateResponse(request, "chart.html", {"result": None, "form": {}})
return templates.TemplateResponse(
request, "chart.html",
{"result": None, "form": default_form(), "location_label": DEFAULT_LOCATION_LABEL},
)
@app.post("/", response_class=HTMLResponse)
@@ -105,7 +110,10 @@ def significators_search(
# ---------------- Interpretacje (wynik obliczeń szukany w bazie) ----------------
@app.get("/interpret", response_class=HTMLResponse)
def interpret_form(request: Request):
return templates.TemplateResponse(request, "interpret.html", {"result": None, "form": {}})
return templates.TemplateResponse(
request, "interpret.html",
{"result": None, "form": default_form(), "location_label": DEFAULT_LOCATION_LABEL},
)
@app.post("/interpret", response_class=HTMLResponse)
@@ -135,7 +143,10 @@ def interpret_run(
# ---------------- Kalendarz (oś czasu z technik + interpretacje) ----------------
@app.get("/timeline", response_class=HTMLResponse)
def timeline_form(request: Request):
return templates.TemplateResponse(request, "timeline.html", {"result": None, "form": {}})
return templates.TemplateResponse(
request, "timeline.html",
{"result": None, "form": default_form(), "location_label": DEFAULT_LOCATION_LABEL},
)
@app.post("/timeline", response_class=HTMLResponse)
@@ -166,6 +177,25 @@ def timeline_run(
return templates.TemplateResponse(request, "timeline.html", ctx)
# ---------------- Geokoder (proxy OSM/Nominatim dla wyszukiwarki lokalizacji) ----------------
@app.get("/geocode")
def geocode_search(q: str = Query("", description="Nazwa / adres / POI do wyszukania")):
"""Nazwa miejsca → kandydaci ze współrzędnymi (dla pola „Szukaj miejsca")."""
try:
return {"results": geocode.search(q)}
except httpx.HTTPError as e:
raise HTTPException(status_code=502, detail=f"Geokoder (OSM) niedostępny: {e}")
@app.get("/reverse")
def geocode_reverse(lat: float, lon: float):
"""Punkt z mapy → nazwa miejsca (po przeciągnięciu pineski / kliknięciu)."""
try:
return geocode.reverse(lat, lon)
except httpx.HTTPError as e:
raise HTTPException(status_code=502, detail=f"Geokoder (OSM) niedostępny: {e}")
@app.get("/health")
def health() -> dict:
return {"status": "ok", "layer": "presentation"}
+116
View File
@@ -0,0 +1,116 @@
// 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');
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);
function setCoords(lat, lon, fromMap) {
latEl.value = lat.toFixed(4);
lonEl.value = lon.toFixed(4);
marker.setLatLng([lat, lon]);
if (!fromMap) map.setView([lat, lon], Math.max(map.getZoom(), 12));
if (fromMap) reverseName(lat, lon);
}
function reverseName(lat, lon) {
fetch('/reverse?lat=' + lat + '&lon=' + lon)
.then(function (r) { return r.ok ? r.json() : null; })
.then(function (d) { if (d && d.name) searchEl.value = 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);
searchEl.value = 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" ----
document.addEventListener('astrololo:coords', function () {
marker.setLatLng([curLat(), curLon()]);
map.setView([curLat(), curLon()], Math.max(map.getZoom(), 12));
});
});
})();
+1
View File
@@ -32,6 +32,7 @@ document.addEventListener('DOMContentLoaded', function () {
function (pos) {
document.querySelector('input[name=lat]').value = pos.coords.latitude.toFixed(4);
document.querySelector('input[name=lon]').value = pos.coords.longitude.toFixed(4);
document.dispatchEvent(new CustomEvent('astrololo:coords')); // zsynchronizuj mapę (geo.js)
say('Lokalizacja pobrana ✓');
},
function (err) {
@@ -64,3 +64,20 @@ tr:last-child td { border-bottom: none; }
.badge { display: inline-block; font-size: .72rem; padding: .05rem .4rem; border-radius: 6px;
background: #2a2d4a; color: var(--accent); margin-left: .3rem; }
.samples td.sig { color: var(--accent); padding-right: 1rem; cursor: help; }
/* Wyszukiwarka lokalizacji + mapa (OSM/Leaflet) */
.geo { margin-top: .9rem; }
.geo-search { display: flex; gap: .5rem; flex-wrap: wrap; }
.geo-search input[type=search] { flex: 1; min-width: 12rem; }
.geo-results { list-style: none; margin: .4rem 0 0; padding: 0; border: 1px solid var(--line);
border-radius: 8px; overflow: hidden; background: #12132a; max-height: 14rem; overflow-y: auto; }
.geo-results li { padding: .5rem .75rem; cursor: pointer; border-bottom: 1px solid var(--line); font-size: .9rem; }
.geo-results li:last-child { border-bottom: none; }
.geo-results li:hover { background: var(--panel); color: #fff; }
.geo-map { height: 320px; margin-top: .6rem; border: 1px solid var(--line); border-radius: 10px; }
.geo-map[hidden] { display: none; }
.geo-attrib { margin: .4rem 0 0; }
.geo-attrib a { color: var(--accent); }
.geo-pin { font-size: 20px; line-height: 24px; text-align: center; }
/* kafelki OSM są jasne — trzymamy kontrolki czytelne na ciemnym tle */
.leaflet-control-attribution { font-size: .68rem; }
@@ -0,0 +1,661 @@
/* required styles */
.leaflet-pane,
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-tile-container,
.leaflet-pane > svg,
.leaflet-pane > canvas,
.leaflet-zoom-box,
.leaflet-image-layer,
.leaflet-layer {
position: absolute;
left: 0;
top: 0;
}
.leaflet-container {
overflow: hidden;
}
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
-webkit-user-drag: none;
}
/* Prevents IE11 from highlighting tiles in blue */
.leaflet-tile::selection {
background: transparent;
}
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
.leaflet-safari .leaflet-tile {
image-rendering: -webkit-optimize-contrast;
}
/* hack that prevents hw layers "stretching" when loading new tiles */
.leaflet-safari .leaflet-tile-container {
width: 1600px;
height: 1600px;
-webkit-transform-origin: 0 0;
}
.leaflet-marker-icon,
.leaflet-marker-shadow {
display: block;
}
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
.leaflet-container .leaflet-overlay-pane svg {
max-width: none !important;
max-height: none !important;
}
.leaflet-container .leaflet-marker-pane img,
.leaflet-container .leaflet-shadow-pane img,
.leaflet-container .leaflet-tile-pane img,
.leaflet-container img.leaflet-image-layer,
.leaflet-container .leaflet-tile {
max-width: none !important;
max-height: none !important;
width: auto;
padding: 0;
}
.leaflet-container img.leaflet-tile {
/* See: https://bugs.chromium.org/p/chromium/issues/detail?id=600120 */
mix-blend-mode: plus-lighter;
}
.leaflet-container.leaflet-touch-zoom {
-ms-touch-action: pan-x pan-y;
touch-action: pan-x pan-y;
}
.leaflet-container.leaflet-touch-drag {
-ms-touch-action: pinch-zoom;
/* Fallback for FF which doesn't support pinch-zoom */
touch-action: none;
touch-action: pinch-zoom;
}
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
-ms-touch-action: none;
touch-action: none;
}
.leaflet-container {
-webkit-tap-highlight-color: transparent;
}
.leaflet-container a {
-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
}
.leaflet-tile {
filter: inherit;
visibility: hidden;
}
.leaflet-tile-loaded {
visibility: inherit;
}
.leaflet-zoom-box {
width: 0;
height: 0;
-moz-box-sizing: border-box;
box-sizing: border-box;
z-index: 800;
}
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
.leaflet-overlay-pane svg {
-moz-user-select: none;
}
.leaflet-pane { z-index: 400; }
.leaflet-tile-pane { z-index: 200; }
.leaflet-overlay-pane { z-index: 400; }
.leaflet-shadow-pane { z-index: 500; }
.leaflet-marker-pane { z-index: 600; }
.leaflet-tooltip-pane { z-index: 650; }
.leaflet-popup-pane { z-index: 700; }
.leaflet-map-pane canvas { z-index: 100; }
.leaflet-map-pane svg { z-index: 200; }
.leaflet-vml-shape {
width: 1px;
height: 1px;
}
.lvml {
behavior: url(#default#VML);
display: inline-block;
position: absolute;
}
/* control positioning */
.leaflet-control {
position: relative;
z-index: 800;
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
.leaflet-top,
.leaflet-bottom {
position: absolute;
z-index: 1000;
pointer-events: none;
}
.leaflet-top {
top: 0;
}
.leaflet-right {
right: 0;
}
.leaflet-bottom {
bottom: 0;
}
.leaflet-left {
left: 0;
}
.leaflet-control {
float: left;
clear: both;
}
.leaflet-right .leaflet-control {
float: right;
}
.leaflet-top .leaflet-control {
margin-top: 10px;
}
.leaflet-bottom .leaflet-control {
margin-bottom: 10px;
}
.leaflet-left .leaflet-control {
margin-left: 10px;
}
.leaflet-right .leaflet-control {
margin-right: 10px;
}
/* zoom and fade animations */
.leaflet-fade-anim .leaflet-popup {
opacity: 0;
-webkit-transition: opacity 0.2s linear;
-moz-transition: opacity 0.2s linear;
transition: opacity 0.2s linear;
}
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
opacity: 1;
}
.leaflet-zoom-animated {
-webkit-transform-origin: 0 0;
-ms-transform-origin: 0 0;
transform-origin: 0 0;
}
svg.leaflet-zoom-animated {
will-change: transform;
}
.leaflet-zoom-anim .leaflet-zoom-animated {
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
}
.leaflet-zoom-anim .leaflet-tile,
.leaflet-pan-anim .leaflet-tile {
-webkit-transition: none;
-moz-transition: none;
transition: none;
}
.leaflet-zoom-anim .leaflet-zoom-hide {
visibility: hidden;
}
/* cursors */
.leaflet-interactive {
cursor: pointer;
}
.leaflet-grab {
cursor: -webkit-grab;
cursor: -moz-grab;
cursor: grab;
}
.leaflet-crosshair,
.leaflet-crosshair .leaflet-interactive {
cursor: crosshair;
}
.leaflet-popup-pane,
.leaflet-control {
cursor: auto;
}
.leaflet-dragging .leaflet-grab,
.leaflet-dragging .leaflet-grab .leaflet-interactive,
.leaflet-dragging .leaflet-marker-draggable {
cursor: move;
cursor: -webkit-grabbing;
cursor: -moz-grabbing;
cursor: grabbing;
}
/* marker & overlays interactivity */
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-image-layer,
.leaflet-pane > svg path,
.leaflet-tile-container {
pointer-events: none;
}
.leaflet-marker-icon.leaflet-interactive,
.leaflet-image-layer.leaflet-interactive,
.leaflet-pane > svg path.leaflet-interactive,
svg.leaflet-image-layer.leaflet-interactive path {
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
/* visual tweaks */
.leaflet-container {
background: #ddd;
outline-offset: 1px;
}
.leaflet-container a {
color: #0078A8;
}
.leaflet-zoom-box {
border: 2px dotted #38f;
background: rgba(255,255,255,0.5);
}
/* general typography */
.leaflet-container {
font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
font-size: 12px;
font-size: 0.75rem;
line-height: 1.5;
}
/* general toolbar styles */
.leaflet-bar {
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
border-radius: 4px;
}
.leaflet-bar a {
background-color: #fff;
border-bottom: 1px solid #ccc;
width: 26px;
height: 26px;
line-height: 26px;
display: block;
text-align: center;
text-decoration: none;
color: black;
}
.leaflet-bar a,
.leaflet-control-layers-toggle {
background-position: 50% 50%;
background-repeat: no-repeat;
display: block;
}
.leaflet-bar a:hover,
.leaflet-bar a:focus {
background-color: #f4f4f4;
}
.leaflet-bar a:first-child {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.leaflet-bar a:last-child {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border-bottom: none;
}
.leaflet-bar a.leaflet-disabled {
cursor: default;
background-color: #f4f4f4;
color: #bbb;
}
.leaflet-touch .leaflet-bar a {
width: 30px;
height: 30px;
line-height: 30px;
}
.leaflet-touch .leaflet-bar a:first-child {
border-top-left-radius: 2px;
border-top-right-radius: 2px;
}
.leaflet-touch .leaflet-bar a:last-child {
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px;
}
/* zoom control */
.leaflet-control-zoom-in,
.leaflet-control-zoom-out {
font: bold 18px 'Lucida Console', Monaco, monospace;
text-indent: 1px;
}
.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
font-size: 22px;
}
/* layers control */
.leaflet-control-layers {
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
background: #fff;
border-radius: 5px;
}
.leaflet-control-layers-toggle {
background-image: url(images/layers.png);
width: 36px;
height: 36px;
}
.leaflet-retina .leaflet-control-layers-toggle {
background-image: url(images/layers-2x.png);
background-size: 26px 26px;
}
.leaflet-touch .leaflet-control-layers-toggle {
width: 44px;
height: 44px;
}
.leaflet-control-layers .leaflet-control-layers-list,
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
display: none;
}
.leaflet-control-layers-expanded .leaflet-control-layers-list {
display: block;
position: relative;
}
.leaflet-control-layers-expanded {
padding: 6px 10px 6px 6px;
color: #333;
background: #fff;
}
.leaflet-control-layers-scrollbar {
overflow-y: scroll;
overflow-x: hidden;
padding-right: 5px;
}
.leaflet-control-layers-selector {
margin-top: 2px;
position: relative;
top: 1px;
}
.leaflet-control-layers label {
display: block;
font-size: 13px;
font-size: 1.08333em;
}
.leaflet-control-layers-separator {
height: 0;
border-top: 1px solid #ddd;
margin: 5px -10px 5px -6px;
}
/* Default icon URLs */
.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */
background-image: url(images/marker-icon.png);
}
/* attribution and scale controls */
.leaflet-container .leaflet-control-attribution {
background: #fff;
background: rgba(255, 255, 255, 0.8);
margin: 0;
}
.leaflet-control-attribution,
.leaflet-control-scale-line {
padding: 0 5px;
color: #333;
line-height: 1.4;
}
.leaflet-control-attribution a {
text-decoration: none;
}
.leaflet-control-attribution a:hover,
.leaflet-control-attribution a:focus {
text-decoration: underline;
}
.leaflet-attribution-flag {
display: inline !important;
vertical-align: baseline !important;
width: 1em;
height: 0.6669em;
}
.leaflet-left .leaflet-control-scale {
margin-left: 5px;
}
.leaflet-bottom .leaflet-control-scale {
margin-bottom: 5px;
}
.leaflet-control-scale-line {
border: 2px solid #777;
border-top: none;
line-height: 1.1;
padding: 2px 5px 1px;
white-space: nowrap;
-moz-box-sizing: border-box;
box-sizing: border-box;
background: rgba(255, 255, 255, 0.8);
text-shadow: 1px 1px #fff;
}
.leaflet-control-scale-line:not(:first-child) {
border-top: 2px solid #777;
border-bottom: none;
margin-top: -2px;
}
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
border-bottom: 2px solid #777;
}
.leaflet-touch .leaflet-control-attribution,
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
box-shadow: none;
}
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
border: 2px solid rgba(0,0,0,0.2);
background-clip: padding-box;
}
/* popup */
.leaflet-popup {
position: absolute;
text-align: center;
margin-bottom: 20px;
}
.leaflet-popup-content-wrapper {
padding: 1px;
text-align: left;
border-radius: 12px;
}
.leaflet-popup-content {
margin: 13px 24px 13px 20px;
line-height: 1.3;
font-size: 13px;
font-size: 1.08333em;
min-height: 1px;
}
.leaflet-popup-content p {
margin: 17px 0;
margin: 1.3em 0;
}
.leaflet-popup-tip-container {
width: 40px;
height: 20px;
position: absolute;
left: 50%;
margin-top: -1px;
margin-left: -20px;
overflow: hidden;
pointer-events: none;
}
.leaflet-popup-tip {
width: 17px;
height: 17px;
padding: 1px;
margin: -10px auto 0;
pointer-events: auto;
-webkit-transform: rotate(45deg);
-moz-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
.leaflet-popup-content-wrapper,
.leaflet-popup-tip {
background: white;
color: #333;
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
}
.leaflet-container a.leaflet-popup-close-button {
position: absolute;
top: 0;
right: 0;
border: none;
text-align: center;
width: 24px;
height: 24px;
font: 16px/24px Tahoma, Verdana, sans-serif;
color: #757575;
text-decoration: none;
background: transparent;
}
.leaflet-container a.leaflet-popup-close-button:hover,
.leaflet-container a.leaflet-popup-close-button:focus {
color: #585858;
}
.leaflet-popup-scrolled {
overflow: auto;
}
.leaflet-oldie .leaflet-popup-content-wrapper {
-ms-zoom: 1;
}
.leaflet-oldie .leaflet-popup-tip {
width: 24px;
margin: 0 auto;
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
}
.leaflet-oldie .leaflet-control-zoom,
.leaflet-oldie .leaflet-control-layers,
.leaflet-oldie .leaflet-popup-content-wrapper,
.leaflet-oldie .leaflet-popup-tip {
border: 1px solid #999;
}
/* div icon */
.leaflet-div-icon {
background: #fff;
border: 1px solid #666;
}
/* Tooltip */
/* Base styles for the element that has a tooltip */
.leaflet-tooltip {
position: absolute;
padding: 6px;
background-color: #fff;
border: 1px solid #fff;
border-radius: 3px;
color: #222;
white-space: nowrap;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
pointer-events: none;
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
}
.leaflet-tooltip.leaflet-interactive {
cursor: pointer;
pointer-events: auto;
}
.leaflet-tooltip-top:before,
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
position: absolute;
pointer-events: none;
border: 6px solid transparent;
background: transparent;
content: "";
}
/* Directions */
.leaflet-tooltip-bottom {
margin-top: 6px;
}
.leaflet-tooltip-top {
margin-top: -6px;
}
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-top:before {
left: 50%;
margin-left: -6px;
}
.leaflet-tooltip-top:before {
bottom: 0;
margin-bottom: -12px;
border-top-color: #fff;
}
.leaflet-tooltip-bottom:before {
top: 0;
margin-top: -12px;
margin-left: -6px;
border-bottom-color: #fff;
}
.leaflet-tooltip-left {
margin-left: -6px;
}
.leaflet-tooltip-right {
margin-left: 6px;
}
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
top: 50%;
margin-top: -6px;
}
.leaflet-tooltip-left:before {
right: 0;
margin-right: -12px;
border-left-color: #fff;
}
.leaflet-tooltip-right:before {
left: 0;
margin-left: -12px;
border-right-color: #fff;
}
/* Printing */
@media print {
/* Prevent printers from removing background-images of controls. */
.leaflet-control {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,20 @@
{# Wyszukiwarka lokalizacji + interaktywna mapa (OpenStreetMap / Leaflet, bez klucza API).
Wklejana do formularzy z polami input[name=lat] / input[name=lon]. Samowystarczalna:
ładuje CSS/JS Leafleta (vendorowane lokalnie) oraz geo.js. Kafelki mapy lecą z OSM. #}
<link rel="stylesheet" href="/static/vendor/leaflet/leaflet.css">
<div class="geo" id="geoPicker">
<div class="geo-search">
<input type="search" id="geoSearch" autocomplete="off"
placeholder="Szukaj miejsca (np. Szpital Barlickiego, Łódź)…">
<button type="button" id="geoSearchBtn" class="ghost">Szukaj</button>
<button type="button" id="geoMapToggle" class="ghost">Ukryj mapę</button>
</div>
<ul id="geoResults" class="geo-results" hidden></ul>
<div id="map" class="geo-map"></div>
<p class="muted small geo-attrib">
Wyszukiwarka i mapa: © <a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noopener">OpenStreetMap</a>
contributors (Nominatim). Kliknij mapę lub przeciągnij pineskę, by ustawić współrzędne.
</p>
</div>
<script defer src="/static/vendor/leaflet/leaflet.js"></script>
<script defer src="/static/geo.js"></script>
@@ -33,10 +33,12 @@
</select>
</label>
</div>
{% include "_location_picker.html" %}
<div class="opts">
<label><input type="checkbox" name="stations" value="true" {{ 'checked' if form.stations else '' }}>
licz stacje planet (wolniejsze)</label>
</div>
{% if location_label %}<p class="muted small">Wstępnie wpisano lokalizację: <strong>{{ location_label }}</strong> ({{ form.lat }}, {{ form.lon }}). Zmień pola lub kliknij „Tu i teraz".</p>{% endif %}
<div class="actions">
<button type="button" id="nowBtn" class="ghost">Tu i teraz</button>
<button type="submit">Policz horoskop</button>
@@ -23,9 +23,11 @@
<input type="number" name="lon" step="0.0001" value="{{ form.lon if form.lon is not none else 0 }}">
</label>
</div>
{% include "_location_picker.html" %}
<div class="opts">
<label><input type="checkbox" name="group" value="true" {{ 'checked' if form.group else '' }}> grupuj identyczne opisy</label>
</div>
{% if location_label %}<p class="muted small">Wstępnie wpisano lokalizację: <strong>{{ location_label }}</strong> ({{ form.lat }}, {{ form.lon }}). Zmień pola lub kliknij „Tu i teraz".</p>{% endif %}
<div class="actions">
<button type="button" id="nowBtn" class="ghost">Tu i teraz</button>
<button type="submit">Szukaj interpretacji</button>
@@ -23,6 +23,8 @@
<input type="number" name="lon" step="0.0001" value="{{ form.lon if form.lon is not none else 0 }}">
</label>
</div>
{% if location_label %}<p class="muted small">Wstępnie wpisano lokalizację: <strong>{{ location_label }}</strong> ({{ form.lat }}, {{ form.lon }}). Zmień pola lub kliknij „Tu i teraz".</p>{% endif %}
{% include "_location_picker.html" %}
<div class="grid">
<label>Zakres od
<input type="date" name="from_date" value="{{ form.from_date or '' }}" required>
View File