From fdbaacb3570ce5392c3bf4979088195ad9ab0cc6 Mon Sep 17 00:00:00 2001 From: Kowalski Date: Sun, 11 Oct 2026 20:11:38 +0200 Subject: [PATCH] =?UTF-8?q?Mnoho=20zm=C4=9Bn=20-=20Zvestovatele=20zru?= =?UTF-8?q?=C5=A1eni,=20Mapy=20cel=C3=A9ho=20obvodu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Model/ObvodFacade.php | 44 +- app/Model/ZaznamFacade.php | 143 +++- app/Model/ZvestovatelFacade.php | 73 ++ app/Presentation/@layout.latte | 5 +- app/Presentation/Home/default.latte | 2 +- app/Presentation/Mapa/MapaPresenter.php | 30 +- app/Presentation/Mapa/default.latte | 21 +- app/Presentation/Obvod/ObvodPresenter.php | 184 ++++- app/Presentation/Obvod/list.latte | 34 +- app/Presentation/Obvod/view.latte | 172 ++++- app/Presentation/Stari/StariPresenter.php | 16 + app/Presentation/Stari/default.latte | 9 + .../Zvestovatel/ZvestovatelPresenter.php | 80 +++ app/Presentation/Zvestovatel/default.latte | 26 + app/Presentation/Zvestovatel/edit.latte | 15 + www/css/base.css | 211 ++++++ www/js/mapa.js | 4 +- www/js/naseptavac.js | 116 ++++ www/js/obvod-nahled.js | 66 ++ www/js/obvody.js | 29 +- www/js/stari.js | 631 ++++++++++++++++++ 21 files changed, 1861 insertions(+), 50 deletions(-) create mode 100644 app/Model/ZvestovatelFacade.php create mode 100644 app/Presentation/Stari/StariPresenter.php create mode 100644 app/Presentation/Stari/default.latte create mode 100644 app/Presentation/Zvestovatel/ZvestovatelPresenter.php create mode 100644 app/Presentation/Zvestovatel/default.latte create mode 100644 app/Presentation/Zvestovatel/edit.latte create mode 100644 www/js/naseptavac.js create mode 100644 www/js/obvod-nahled.js create mode 100644 www/js/stari.js diff --git a/app/Model/ObvodFacade.php b/app/Model/ObvodFacade.php index 5774970..33d0bbf 100644 --- a/app/Model/ObvodFacade.php +++ b/app/Model/ObvodFacade.php @@ -34,14 +34,50 @@ final class ObvodFacade return $this->database->fetch($sql, $id); } + public const SKUPINA_COOKIE = 'obvodSkupina'; + /** * Základní informace o obvodech. - * @return Nette\Database\Table\ActiveRow|null + * @return array */ - public function getObvody(): array + public function getObvody(?string $skupina = null): array { - $sql = "SELECT * FROM OBVOD"; - return $this->database->fetchAll($sql ); + if ($skupina === null || $skupina === '') { + return $this->database->fetchAll('SELECT * FROM OBVOD ORDER BY CISLO'); + } + + return $this->database->fetchAll( + 'SELECT * FROM OBVOD WHERE SKUPINA = ? ORDER BY CISLO', + $skupina, + ); + } + + /** + * @return list + */ + public function getSkupiny(): array + { + $radky = $this->database->fetchAll( + "SELECT DISTINCT SKUPINA + FROM OBVOD + WHERE SKUPINA IS NOT NULL AND LTRIM(RTRIM(SKUPINA)) <> '' + ORDER BY SKUPINA", + ); + + return array_map(static fn(Row $radek): string => (string) $radek->SKUPINA, $radky); + } + + public function vyberSkupinu(?string $pozadovana, ?string $ulozena): string + { + $skupiny = $this->getSkupiny(); + if ($pozadovana !== null && in_array($pozadovana, $skupiny, true)) { + return $pozadovana; + } + if ($ulozena !== null && in_array($ulozena, $skupiny, true)) { + return $ulozena; + } + + return $skupiny[0] ?? ''; } } \ No newline at end of file diff --git a/app/Model/ZaznamFacade.php b/app/Model/ZaznamFacade.php index dd5aedd..e2586c9 100644 --- a/app/Model/ZaznamFacade.php +++ b/app/Model/ZaznamFacade.php @@ -28,8 +28,149 @@ final class ZaznamFacade */ public function getZaznamyObvodu(int $obvod): array { - $sql = "SELECT * FROM ZAZNAM WHERE OBVOD = ?"; + $sql = 'SELECT * FROM ZAZNAM WHERE OBVOD = ? AND DELETED = 0 ORDER BY VYDANO DESC, ID DESC'; return $this->database->fetchAll($sql, $obvod); } + /** + * Jména zvěstovatelů pro výběr při přidělení. + * @return array + */ + public function getZvestovatele(): array + { + $rows = $this->database->fetchAll( + 'SELECT DISTINCT JMENO FROM ZAZNAM WHERE DELETED = 0 ORDER BY JMENO' + ); + $jmena = []; + foreach ($rows as $row) { + $jmena[$row->JMENO] = $row->JMENO; + } + return $jmena; + } + + /** + * Přidělení, která ještě nebyla vrácena. + */ + public function getOtevrene(int $obvod): array + { + return $this->database->fetchAll( + 'SELECT * FROM ZAZNAM WHERE OBVOD = ? AND DELETED = 0 AND VRACENO IS NULL ORDER BY VYDANO, ID', + $obvod + ); + } + + public function getZaznam(int $id): Row|null + { + return $this->database->fetch('SELECT * FROM ZAZNAM WHERE ID = ? AND DELETED = 0', $id); + } + + public function pridej(int $obvod, string $jmeno, \DateTimeInterface $vydano): void + { + $this->database->query( + 'INSERT INTO ZAZNAM (OBVOD, JMENO, VYDANO, VRACENO, DELETED) VALUES (?, ?, ?, NULL, 0)', + $obvod, + $jmeno, + $vydano + ); + } + + public function uprav(int $id, int $obvod, string $jmeno, \DateTimeInterface $vydano, ?\DateTimeInterface $vraceno): bool + { + $zaznam = $this->getZaznam($id); + if ($zaznam === null || (int) $zaznam->OBVOD !== $obvod) { + return false; + } + + $this->database->query( + 'UPDATE ZAZNAM SET JMENO = ?, VYDANO = ?, VRACENO = ? WHERE ID = ? AND OBVOD = ? AND DELETED = 0', + $jmeno, + $vydano, + $vraceno, + $id, + $obvod + ); + return true; + } + + public function smaz(int $id, int $obvod): bool + { + $zaznam = $this->getZaznam($id); + if ($zaznam === null || (int) $zaznam->OBVOD !== $obvod) { + return false; + } + + $this->database->query( + 'UPDATE ZAZNAM SET DELETED = 1 WHERE ID = ? AND OBVOD = ? AND DELETED = 0', + $id, + $obvod + ); + return true; + } + + /** + * Stáří obvodů podle posledního vrácení a příznak probíhajícího přidělení. + * Klíčem je číslo obvodu. + * + * @return array + */ + public function getStariObvodu(): array + { + $radky = $this->database->fetchAll( + 'SELECT o.ID, o.CISLO, + MAX(CASE WHEN z.VRACENO IS NOT NULL THEN z.VRACENO END) AS POSLEDNI, + MIN(CASE WHEN z.VRACENO IS NULL THEN z.VYDANO END) AS PRIDELENO, + SUM(CASE WHEN z.ID IS NOT NULL AND z.VRACENO IS NULL THEN 1 ELSE 0 END) AS OTEVRENE + FROM OBVOD o + LEFT JOIN ZAZNAM z ON z.OBVOD = o.ID AND z.DELETED = 0 + GROUP BY o.ID, o.CISLO' + ); + + $dnes = new \DateTimeImmutable('today'); + $vysledek = []; + foreach ($radky as $radek) { + $mesice = null; + $vraceno = null; + if ($radek->POSLEDNI !== null) { + $datum = $radek->POSLEDNI instanceof \DateTimeInterface + ? \DateTimeImmutable::createFromInterface($radek->POSLEDNI) + : new \DateTimeImmutable((string) $radek->POSLEDNI); + $vraceno = $datum->format('j. n. Y'); + $rozdil = $datum->diff($dnes); + $mesice = $rozdil->invert === 1 ? 0 : ($rozdil->y * 12 + $rozdil->m); + } + + $prideleni = null; + $prideleniDatum = null; + if ($radek->PRIDELENO !== null) { + $zacatek = $radek->PRIDELENO instanceof \DateTimeInterface + ? \DateTimeImmutable::createFromInterface($radek->PRIDELENO) + : new \DateTimeImmutable((string) $radek->PRIDELENO); + $prideleniDatum = $zacatek->format('j. n. Y'); + $odPrideleni = $zacatek->diff($dnes); + $prideleni = $odPrideleni->invert === 1 ? 0 : ($odPrideleni->y * 12 + $odPrideleni->m); + } + + $vysledek[(int) $radek->CISLO] = [ + 'id' => (int) $radek->ID, + 'mesice' => $mesice, + 'probiha' => (int) $radek->OTEVRENE > 0, + 'vraceno' => $vraceno, + 'prideleni' => $prideleni, + 'prideleniDatum' => $prideleniDatum, + ]; + } + + return $vysledek; + } + + public function vrat(int $id, int $obvod, \DateTimeInterface $vraceno): void + { + $this->database->query( + 'UPDATE ZAZNAM SET VRACENO = ? WHERE ID = ? AND OBVOD = ? AND DELETED = 0 AND VRACENO IS NULL', + $vraceno, + $id, + $obvod + ); + } + } \ No newline at end of file diff --git a/app/Model/ZvestovatelFacade.php b/app/Model/ZvestovatelFacade.php new file mode 100644 index 0000000..3442178 --- /dev/null +++ b/app/Model/ZvestovatelFacade.php @@ -0,0 +1,73 @@ + + */ + public function getVsechny(): array + { + return $this->database->fetchAll( + 'SELECT JMENO, COUNT(*) AS POCET + FROM ZAZNAM + WHERE DELETED = 0 + GROUP BY JMENO + ORDER BY JMENO' + ); + } + + public function get(string $jmeno): Row|null + { + return $this->database->fetch( + 'SELECT JMENO, COUNT(*) AS POCET + FROM ZAZNAM + WHERE DELETED = 0 AND JMENO = ? + GROUP BY JMENO', + $jmeno + ); + } + + public function existujeJmeno(string $jmeno, ?string $krome = null): bool + { + if ($krome === null) { + return $this->get($jmeno) !== null; + } + + return $this->database->fetchField( + 'SELECT TOP 1 ID FROM ZAZNAM WHERE DELETED = 0 AND JMENO = ? AND JMENO <> ?', + $jmeno, + $krome + ) !== null; + } + + /** + * Přejmenuje zvěstovatele ve všech jeho záznamech. + */ + public function uprav(string $puvodni, string $jmeno): void + { + if ($this->get($puvodni) === null || $puvodni === $jmeno) { + return; + } + + $this->database->query( + 'UPDATE ZAZNAM SET JMENO = ? WHERE JMENO = ?', + $jmeno, + $puvodni + ); + } +} diff --git a/app/Presentation/@layout.latte b/app/Presentation/@layout.latte index e151304..38a09b0 100644 --- a/app/Presentation/@layout.latte +++ b/app/Presentation/@layout.latte @@ -28,7 +28,7 @@ {* naše vlastní styly *} - + {ifset title}{include title|stripHtml} | {/ifset}Obvody @@ -54,7 +54,8 @@ {if $user->isLoggedIn()} Obvody Celý obvod - Seznam obvodů + Obvody + Zvěstovatelé Odhlásit {else} Přihlásit diff --git a/app/Presentation/Home/default.latte b/app/Presentation/Home/default.latte index 209f66a..5bb5a5d 100644 --- a/app/Presentation/Home/default.latte +++ b/app/Presentation/Home/default.latte @@ -1,7 +1,7 @@ {* This is the welcome page, you can delete it *} {block content} -

Seznam obvodů

+

Obvody

{* Tlačítka: *} diff --git a/app/Presentation/Mapa/MapaPresenter.php b/app/Presentation/Mapa/MapaPresenter.php index 9803090..e51126c 100644 --- a/app/Presentation/Mapa/MapaPresenter.php +++ b/app/Presentation/Mapa/MapaPresenter.php @@ -4,17 +4,41 @@ declare(strict_types=1); namespace App\Presentation\Mapa; +use App\Model\ZaznamFacade; use Nette; +use Nette\Utils\Json; final class MapaPresenter extends Nette\Application\UI\Presenter { - public function beforeRender(): void - { + /** Od tohoto počtu měsíců je obvod ve filtru stáří vykreslený červeně. */ + public const STUPNICE_MESICU = 24; + + /** Od tohoto počtu měsíců je rozpracovaný obvod vykreslený červeně. */ + public const STUPNICE_PRIDELENI = 12; + + public function __construct( + private ZaznamFacade $zaznam, + ) { } - public function renderDefault(int $obvod = null): void + public function renderDefault(?int $obvod = null, ?string $filtr = null): void { + $povolene = ['obvody', 'stari', 'nedokoncene']; + if (!in_array($filtr, $povolene, true)) { + $filtr = 'obvody'; + } + + $stari = $this->zaznam->getStariObvodu(); + foreach ($stari as &$polozka) { + $polozka['url'] = $this->link('Obvod:view', $polozka['id']); + } + unset($polozka); + $this->template->obvod_id = $obvod; + $this->template->filtr = $filtr; + $this->template->maxMesicu = self::STUPNICE_MESICU; + $this->template->maxPrideleni = self::STUPNICE_PRIDELENI; + $this->template->stariJson = Json::encode($stari, htmlSafe: true, forceObjects: true); } } diff --git a/app/Presentation/Mapa/default.latte b/app/Presentation/Mapa/default.latte index 70f8905..1de7784 100644 --- a/app/Presentation/Mapa/default.latte +++ b/app/Presentation/Mapa/default.latte @@ -1,11 +1,14 @@ -{* Zobrazení obvodu na mapě. *} +{block title}Celý obvod{/block} {block content} -
- - - - - +
+ + + + +{/block} diff --git a/app/Presentation/Obvod/ObvodPresenter.php b/app/Presentation/Obvod/ObvodPresenter.php index d73bd0d..0fbefac 100644 --- a/app/Presentation/Obvod/ObvodPresenter.php +++ b/app/Presentation/Obvod/ObvodPresenter.php @@ -7,10 +7,15 @@ namespace App\Presentation\Obvod; use App\Model\ObvodFacade; use App\Model\ZaznamFacade; use Nette; +use Nette\Application\UI\Form; +use Nette\Application\UI\Multiplier; +use Nette\Utils\ArrayHash; final class ObvodPresenter extends Nette\Application\UI\Presenter { + private string $skupina = ''; + public function __construct( private ObvodFacade $obvod, private ZaznamFacade $zaznam @@ -21,14 +26,191 @@ final class ObvodPresenter extends Nette\Application\UI\Presenter { } + public function actionList(?string $skupina = null): void + { + $this->skupina = $this->obvod->vyberSkupinu( + $skupina, + $this->getHttpRequest()->getCookie(ObvodFacade::SKUPINA_COOKIE), + ); + if ($skupina !== null && $skupina === $this->skupina && $this->skupina !== '') { + $this->getHttpResponse()->setCookie(ObvodFacade::SKUPINA_COOKIE, $this->skupina, '+1 year', '/'); + } + } + public function renderList(): void { - $this->template->obvody = $this->obvod->getObvody(); + $this->template->skupina = $this->skupina; + $this->template->skupiny = $this->obvod->getSkupiny(); + $this->template->obvody = $this->obvod->getObvody($this->skupina); } public function renderView(int $id = null): void { $this->template->obvod = $this->obvod->getObvod($id); $this->template->zaznamy = $this->zaznam->getZaznamyObvodu($id); + $this->template->zvestovateleJson = json_encode( + array_values($this->zaznam->getZvestovatele()), + JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_THROW_ON_ERROR + ); + $this->template->otevrene = $id === null ? [] : $this->zaznam->getOtevrene($id); + } + + protected function createComponentPridelitForm(): Form + { + $form = new Form; + $form->addText('zvestovatel', 'Zvěstovatel') + ->setRequired('Zadejte zvěstovatele.') + ->setMaxLength(100); + $form->addDate('datum', 'Datum přidělení') + ->setRequired('Zadejte datum přidělení.') + ->setDefaultValue(new \DateTimeImmutable('today')); + $form->addSubmit('send', 'Přidělit'); + $form->addProtection('Platnost formuláře vypršela, odešlete ho znovu.'); + $form->onSuccess[] = $this->pridelitFormSucceeded(...); + return $form; + } + + private function pridelitFormSucceeded(Form $form, ArrayHash $values): void + { + $id = $this->obvodId(); + $jmeno = trim((string) $values->zvestovatel); + if ($jmeno === '') { + $form->addError('Zadejte zvěstovatele.'); + return; + } + if (mb_strlen($jmeno) > 100) { + $form->addError('Jméno zvěstovatele je příliš dlouhé.'); + return; + } + + $this->zaznam->pridej($id, $jmeno, $this->den($values->datum)); + $this->flashMessage('Obvod byl přidělen.'); + $this->redirect('this'); + } + + protected function createComponentVratitForm(): Form + { + $form = new Form; + $polozky = []; + foreach ($this->zaznam->getOtevrene($this->obvodId()) as $zaznam) { + $polozky[$zaznam->ID] = $zaznam->JMENO . ' (přiděleno ' . $this->den($zaznam->VYDANO)->format('d.m.Y') . ')'; + } + $form->addSelect('zaznam', 'Záznam', $polozky) + ->setRequired('Vyberte záznam k vrácení.'); + $form->addDate('datum', 'Datum vrácení') + ->setRequired('Zadejte datum vrácení.') + ->setDefaultValue(new \DateTimeImmutable('today')); + $form->addSubmit('send', 'Vrátit'); + $form->addProtection('Platnost formuláře vypršela, odešlete ho znovu.'); + $form->onSuccess[] = $this->vratitFormSucceeded(...); + return $form; + } + + private function vratitFormSucceeded(Form $form, ArrayHash $values): void + { + $obvodId = $this->obvodId(); + $zaznam = $this->zaznam->getZaznam((int) $values->zaznam); + if ($zaznam === null || (int) $zaznam->OBVOD !== $obvodId || $zaznam->VRACENO !== null) { + $form->addError('Tento záznam už nelze vrátit.'); + return; + } + + $vraceno = $this->den($values->datum); + if ($this->den($zaznam->VYDANO) > $vraceno) { + $form->addError('Datum přidělení nesmí být později než datum vrácení.'); + return; + } + + $this->zaznam->vrat((int) $zaznam->ID, $obvodId, $vraceno); + $this->flashMessage('Obvod byl vrácen.'); + $this->redirect('this'); + } + + protected function createComponentUpravitForm(): Form + { + $form = new Form; + $form->addHidden('id'); + $form->addText('zvestovatel', 'Zvěstovatel') + ->setRequired('Zadejte zvěstovatele.') + ->setMaxLength(100); + $form->addDate('vydano', 'Datum přidělení') + ->setRequired('Zadejte datum přidělení.'); + $form->addDate('vraceno', 'Datum vrácení'); + $form->addSubmit('send', 'Uložit'); + $form->addProtection('Platnost formuláře vypršela, odešlete ho znovu.'); + $form->onSuccess[] = $this->upravitFormSucceeded(...); + return $form; + } + + private function upravitFormSucceeded(Form $form, ArrayHash $values): void + { + $obvodId = $this->obvodId(); + $zaznam = $this->zaznam->getZaznam((int) $values->id); + if ($zaznam === null || (int) $zaznam->OBVOD !== $obvodId) { + $form->addError('Tento záznam už nelze upravit.'); + return; + } + + $jmeno = trim((string) $values->zvestovatel); + if ($jmeno === '') { + $form->addError('Zadejte zvěstovatele.'); + return; + } + if (mb_strlen($jmeno) > 100) { + $form->addError('Jméno zvěstovatele je příliš dlouhé.'); + return; + } + + $vydano = $this->den($values->vydano); + $vraceno = $values->vraceno !== null && $values->vraceno !== '' + ? $this->den($values->vraceno) + : null; + if ($vraceno !== null && $vydano > $vraceno) { + $form->addError('Datum přidělení nesmí být později než datum vrácení.'); + return; + } + + if (!$this->zaznam->uprav((int) $zaznam->ID, $obvodId, $jmeno, $vydano, $vraceno)) { + $form->addError('Tento záznam už nelze upravit.'); + return; + } + + $this->flashMessage('Záznam byl upraven.'); + $this->redirect('this'); + } + + protected function createComponentSmazat(): Multiplier + { + return new Multiplier(function (string $id): Form { + $form = new Form; + $form->addSubmit('send', 'Smazat'); + $form->addProtection('Platnost formuláře vypršela, odešlete ho znovu.'); + $form->onSuccess[] = function () use ($id): void { + if ($this->zaznam->smaz((int) $id, $this->obvodId())) { + $this->flashMessage('Záznam byl smazán.'); + } else { + $this->flashMessage('Tento záznam už nelze smazat.'); + } + $this->redirect('this'); + }; + return $form; + }); + } + + private function obvodId(): int + { + $id = $this->getParameter('id'); + if ($id === null || $id === '') { + $this->error('Obvod nenalezen.', Nette\Http\IResponse::S404_NotFound); + } + return (int) $id; + } + + private function den(mixed $value): \DateTimeImmutable + { + $datum = $value instanceof \DateTimeInterface + ? \DateTimeImmutable::createFromInterface($value) + : new \DateTimeImmutable((string) $value); + return $datum->setTime(0, 0); } } diff --git a/app/Presentation/Obvod/list.latte b/app/Presentation/Obvod/list.latte index a67882a..e1f71cf 100644 --- a/app/Presentation/Obvod/list.latte +++ b/app/Presentation/Obvod/list.latte @@ -1,7 +1,31 @@ {block content} -{foreach $obvody as $obvod} - - {$obvod->CISLO} - -{/foreach} \ No newline at end of file +
+

Obvody

+
+
+ {$s} +
+
+ + + + + + + + + + + {foreach $obvody as $obvod} + + + + + + {/foreach} + +
ČísloNázevAkce
{$obvod->CISLO}{$obvod->NAZEV} + Otevřít +
+
\ No newline at end of file diff --git a/app/Presentation/Obvod/view.latte b/app/Presentation/Obvod/view.latte index 6fc8de1..e4861ab 100644 --- a/app/Presentation/Obvod/view.latte +++ b/app/Presentation/Obvod/view.latte @@ -1,12 +1,18 @@ {block content} +
Číslo: {$obvod->CISLO}
Název: {$obvod->NAZEV}
Skupina: {$obvod->SKUPINA}
Mapa + +
Záznamy:
-Přidělit -Vrátit + + @@ -19,14 +25,170 @@ Skupina: {$obvod->SKUPINA}
{foreach $zaznamy as $zaznam} - + + {/foreach} -
{$zaznam->ZAZNAM}{$zaznam->JMENO} {date_format($zaznam->VYDANO,"d.m.Y")} - {date_format($zaznam->VRACENO,"d.m.Y")} + {if $zaznam->VRACENO}{date_format($zaznam->VRACENO,"d.m.Y")}{/if} + +
+ + {form "smazat-$zaznam->ID", class: 'd-inline'} + + {/form} +
\ No newline at end of file + +
+ + + + + + + + + + + \ No newline at end of file diff --git a/app/Presentation/Stari/StariPresenter.php b/app/Presentation/Stari/StariPresenter.php new file mode 100644 index 0000000..af85035 --- /dev/null +++ b/app/Presentation/Stari/StariPresenter.php @@ -0,0 +1,16 @@ +redirect(':Mapa:default', ['filtr' => 'stari']); + } +} diff --git a/app/Presentation/Stari/default.latte b/app/Presentation/Stari/default.latte new file mode 100644 index 0000000..14747d5 --- /dev/null +++ b/app/Presentation/Stari/default.latte @@ -0,0 +1,9 @@ +{block title}Stáří obvodů{/block} + +{block content} +
+ + + + +{/block} diff --git a/app/Presentation/Zvestovatel/ZvestovatelPresenter.php b/app/Presentation/Zvestovatel/ZvestovatelPresenter.php new file mode 100644 index 0000000..c54c8ea --- /dev/null +++ b/app/Presentation/Zvestovatel/ZvestovatelPresenter.php @@ -0,0 +1,80 @@ +template->zvestovatele = $this->zvestovatele->getVsechny(); + } + + public function actionEdit(string $jmeno = ''): void + { + $jmeno = trim($jmeno); + if ($this->zvestovatele->get($jmeno) === null) { + $this->error('Zvěstovatel nenalezen.', Nette\Http\IResponse::S404_NotFound); + } + } + + public function renderEdit(string $jmeno = ''): void + { + $jmeno = trim($jmeno); + $this['zvestovatelForm']->setDefaults([ + 'puvodni' => $jmeno, + 'jmeno' => $jmeno, + ]); + } + + protected function createComponentZvestovatelForm(): Form + { + $form = new Form; + $form->addHidden('puvodni'); + $form->addText('jmeno', 'Jméno') + ->setRequired('Zadejte jméno zvěstovatele.') + ->setMaxLength(100); + $form->addSubmit('send', 'Uložit'); + $form->addProtection('Platnost formuláře vypršela, odešlete ho znovu.'); + $form->onSuccess[] = $this->zvestovatelFormSucceeded(...); + return $form; + } + + private function zvestovatelFormSucceeded(Form $form, ArrayHash $values): void + { + $jmeno = trim((string) $values->jmeno); + $puvodni = trim((string) $values->puvodni); + if ($jmeno === '') { + $form->addError('Zadejte jméno zvěstovatele.'); + return; + } + if (mb_strlen($jmeno) > 100) { + $form->addError('Jméno zvěstovatele je příliš dlouhé.'); + return; + } + if ($this->zvestovatele->get($puvodni) === null) { + $form->addError('Zvěstovatel nenalezen.'); + return; + } + if ($jmeno !== $puvodni && $this->zvestovatele->existujeJmeno($jmeno, $puvodni)) { + $form->addError('Zvěstovatel s tímto jménem už existuje.'); + return; + } + + $this->zvestovatele->uprav($puvodni, $jmeno); + $this->flashMessage('Zvěstovatel byl upraven.'); + $this->redirect('default'); + } +} diff --git a/app/Presentation/Zvestovatel/default.latte b/app/Presentation/Zvestovatel/default.latte new file mode 100644 index 0000000..7525b97 --- /dev/null +++ b/app/Presentation/Zvestovatel/default.latte @@ -0,0 +1,26 @@ +{block content} + +
+

Zvěstovatelé

+ + + + + + + + + + + {foreach $zvestovatele as $zvestovatel} + + + + + + {/foreach} + +
JménoZáznamyAkce
{$zvestovatel->JMENO}{$zvestovatel->POCET} + Upravit +
+
diff --git a/app/Presentation/Zvestovatel/edit.latte b/app/Presentation/Zvestovatel/edit.latte new file mode 100644 index 0000000..d78c120 --- /dev/null +++ b/app/Presentation/Zvestovatel/edit.latte @@ -0,0 +1,15 @@ +{block content} + +
+

Upravit zvěstovatele

+
+ +
{$error}
+
+ + +
+ + Zpět +
+
diff --git a/www/css/base.css b/www/css/base.css index ca2280c..ec2e3ed 100644 --- a/www/css/base.css +++ b/www/css/base.css @@ -7,6 +7,217 @@ html, body, #map { width: 100vw; } +body:has(#map[data-max]) { + height: 100vh; + display: flex; + flex-direction: column; + overflow: hidden; +} + +#map[data-max] { + flex: 1 1 auto; + height: auto; + min-height: 0; +} + +.naseptavac { + position: relative; +} + +.naseptavac input { + min-height: 2.75rem; + font-size: 1rem; + border-radius: 0.8rem; + border-color: #d5e3f5; +} + +.naseptavac input:focus { + border-color: #2652a8; + box-shadow: 0 0 0 0.2rem rgba(38, 82, 168, 0.16); +} + +.naseptavac-nabidka { + position: absolute; + z-index: 5; + left: 0; + right: 0; + top: calc(100% + 0.35rem); + padding: 0.35rem; + background: #fff; + border: 0; + border-radius: 0.9rem; + box-shadow: 0 12px 32px rgba(25, 55, 110, 0.18); +} + +.naseptavac-polozka { + display: flex; + align-items: center; + width: 100%; + min-height: 2.75rem; + text-align: left; + border: 0; + border-radius: 0.65rem; + background: transparent; + color: #1a3a6b; + padding: 0.55rem 0.8rem; + font-size: 1rem; + line-height: 1.25; +} + +.naseptavac-polozka.is-active, +.naseptavac-polozka:hover { + background: #e3f2fd; + color: #2652a8; +} + +@media (max-width: 576px) { + #modalPridelit .modal-dialog { + margin: 0.5rem; + } + + #modalPridelit .modal-content { + border: 0; + border-radius: 1rem; + } + + .naseptavac-nabidka { + position: static; + margin-top: 0.4rem; + } + + .naseptavac input, + .naseptavac-polozka { + min-height: 3rem; + font-size: 1.05rem; + } +} + +.obvod-znacka, +.obvod-skupina { + background: none; + border: none; +} + +.obvod-znacka-telo, +.obvod-skupina-telo { + display: flex; + flex-direction: column; + align-items: center; + line-height: 1; +} + +.obvod-kolecko { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: 50%; + border: 2px solid transparent; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.35); + font-size: 12px; + font-weight: 700; + box-sizing: border-box; +} + +.obvod-badge { + margin-top: 2px; + padding: 1px 5px 2px; + border-radius: 8px; + border: 1px solid rgba(0, 0, 0, 0.28); + font-size: 10px; + font-weight: 700; + white-space: nowrap; + text-align: center; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.25); + box-sizing: border-box; +} + +.obvod-skupina .obvod-kolecko { + width: 40px; + height: 40px; + font-size: 13px; +} + +.mapa-filtr { + display: flex; + flex-wrap: wrap; + gap: 4px; + max-width: calc(100vw - 4rem); + padding: 4px; + background: rgba(255, 255, 255, 0.94); + border-radius: 8px; + box-shadow: 0 1px 6px rgba(0, 0, 0, 0.25); +} + +.mapa-filtr button { + border: 0; + border-radius: 6px; + background: transparent; + color: #1a3a6b; + font-size: 13px; + font-weight: 700; + padding: 0.4rem 0.7rem; + line-height: 1.2; +} + +.mapa-filtr button.is-active { + background: #2652a8; + color: #fff; +} + +.stari-legenda { + background: rgba(255, 255, 255, 0.94); + border-radius: 6px; + box-shadow: 0 1px 6px rgba(0, 0, 0, 0.25); + padding: 8px 10px; + width: 210px; + color: #1a1a1a; + font-size: 12px; + line-height: 1.3; +} + +.stari-legenda-nadpis { + font-weight: 700; + margin-bottom: 6px; +} + +.stari-legenda-skala { + height: 10px; + border-radius: 5px; + background: linear-gradient(to right, rgb(27, 158, 62), rgb(230, 184, 0), rgb(198, 40, 40)); +} + +.stari-legenda-popisky { + display: flex; + justify-content: space-between; + margin-top: 2px; +} + +.stari-legenda-seda { + display: flex; + align-items: center; + gap: 6px; + margin-top: 6px; +} + +.stari-legenda-seda span { + width: 14px; + height: 14px; + border-radius: 2px; + background: #9e9e9e; + border: 2px solid #616161; + box-sizing: border-box; +} + +#obvod-nahled { + height: 42vh; + min-height: 220px; + width: 100%; + border: 1px solid #90caf9; + border-radius: 4px; +} + @media (orientation: portrait) { diff --git a/www/js/mapa.js b/www/js/mapa.js index e43c862..4411ae0 100644 --- a/www/js/mapa.js +++ b/www/js/mapa.js @@ -19,8 +19,8 @@ var osm = L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { // Výchozí bod + podkladové mapy: var map = L.map('map', { center: [50.853501146, 14.8425], // Hrádek - zoom: 16, - layers: [mapyCZ, mapyCZletecka, osm] + zoom: 13, + layers: [mapyCZ] }); // Rámeček s výběrem podkladů: diff --git a/www/js/naseptavac.js b/www/js/naseptavac.js new file mode 100644 index 0000000..ced3d36 --- /dev/null +++ b/www/js/naseptavac.js @@ -0,0 +1,116 @@ +(function () { + var dataEl = document.getElementById('zvestovatele-data'); + var jmena = []; + + try { + jmena = JSON.parse(dataEl ? dataEl.textContent || '[]' : '[]'); + } catch (e) { + jmena = []; + } + + document.querySelectorAll('[data-naseptavac]').forEach(function (root) { + var input = root.querySelector('input'); + var nabidka = root.querySelector('.naseptavac-nabidka'); + var aktivni = -1; + if (!input || !nabidka) { + return; + } + + function klic(text) { + return text.normalize('NFD').replace(/\p{M}/gu, '').toLocaleLowerCase('cs'); + } + + function shody() { + var dotaz = klic(input.value.trim()); + if (dotaz === '') { + return []; + } + + var zacina = []; + var obsahuje = []; + jmena.forEach(function (jmeno) { + var pozice = klic(jmeno).indexOf(dotaz); + if (pozice === 0) { + zacina.push(jmeno); + } else if (pozice > 0) { + obsahuje.push(jmeno); + } + }); + + return zacina.concat(obsahuje).slice(0, 5); + } + + function zavri() { + nabidka.hidden = true; + nabidka.replaceChildren(); + input.setAttribute('aria-expanded', 'false'); + aktivni = -1; + } + + function oznac() { + var polozky = nabidka.querySelectorAll('.naseptavac-polozka'); + polozky.forEach(function (polozka, index) { + polozka.classList.toggle('is-active', index === aktivni); + }); + if (aktivni >= 0 && polozky[aktivni]) { + polozky[aktivni].scrollIntoView({ block: 'nearest' }); + } + } + + function vykresli() { + var polozky = shody(); + nabidka.replaceChildren(); + aktivni = -1; + if (!polozky.length) { + zavri(); + return; + } + + polozky.forEach(function (jmeno) { + var tlacitko = document.createElement('button'); + tlacitko.type = 'button'; + tlacitko.className = 'naseptavac-polozka'; + tlacitko.setAttribute('role', 'option'); + tlacitko.textContent = jmeno; + tlacitko.addEventListener('pointerdown', function (event) { + event.preventDefault(); + input.value = jmeno; + zavri(); + }); + nabidka.appendChild(tlacitko); + }); + + nabidka.hidden = false; + input.setAttribute('aria-expanded', 'true'); + } + + input.addEventListener('input', vykresli); + input.addEventListener('focus', vykresli); + input.addEventListener('blur', function () { + setTimeout(zavri, 150); + }); + input.addEventListener('keydown', function (event) { + var polozky = nabidka.querySelectorAll('.naseptavac-polozka'); + if (event.key === 'Escape') { + zavri(); + return; + } + if (nabidka.hidden || !polozky.length) { + return; + } + if (event.key === 'ArrowDown') { + event.preventDefault(); + aktivni = Math.min(aktivni + 1, polozky.length - 1); + oznac(); + } else if (event.key === 'ArrowUp') { + event.preventDefault(); + aktivni = Math.max(aktivni - 1, 0); + oznac(); + } else if (event.key === 'Enter' && aktivni >= 0) { + event.preventDefault(); + input.value = polozky[aktivni].textContent; + zavri(); + } + }); + }); +})(); diff --git a/www/js/obvod-nahled.js b/www/js/obvod-nahled.js new file mode 100644 index 0000000..9e9c40a --- /dev/null +++ b/www/js/obvod-nahled.js @@ -0,0 +1,66 @@ +// Náhled obvodu v detailu — stejný podklad a rámeček jako na celé mapě. +(function () { + var el = document.getElementById('obvod-nahled'); + if (!el || typeof L === 'undefined') { + return; + } + + var obvodId = el.getAttribute('data-obvod'); + var geojsonUrl = el.getAttribute('data-geojson'); + if (!obvodId || !geojsonUrl) { + return; + } + + var API_KEY = 'Uw8gm6JPZpnT3GfNnh8adARpQI-WBGy6QaY9g6VhD74'; + var mapyCZ = L.tileLayer('https://api.mapy.cz/v1/maptiles/basic/256/{z}/{x}/{y}?apikey=' + API_KEY, { + minZoom: 0, + maxZoom: 19, + attribution: '© Seznam.cz a.s. a další', + }); + + var map = L.map(el, { + center: [50.853501146, 14.8425], + zoom: 14, + layers: [mapyCZ], + scrollWheelZoom: false, + }); + + var LogoControl = L.Control.extend({ + options: { position: 'bottomleft' }, + onAdd: function () { + var container = L.DomUtil.create('div'); + var link = L.DomUtil.create('a', '', container); + link.setAttribute('href', 'http://mapy.cz/'); + link.setAttribute('target', '_blank'); + link.innerHTML = 'Mapy.cz'; + L.DomEvent.disableClickPropagation(link); + return container; + }, + }); + new LogoControl().addTo(map); + + function style() { + return { + fillColor: '#FD8D3C', + weight: 2, + opacity: 1, + color: 'blue', + dashArray: '3', + fillOpacity: 0.05, + }; + } + + $.getJSON(geojsonUrl, function (data) { + var layer = L.geoJSON(data, { + style: style, + filter: function (feature) { + return feature.properties && String(feature.properties.number) === String(obvodId); + }, + }).addTo(map); + + if (layer.getLayers().length) { + map.fitBounds(layer.getBounds(), { padding: [16, 16] }); + } + map.invalidateSize(); + }); +})(); diff --git a/www/js/obvody.js b/www/js/obvody.js index 00286aa..afe4c84 100644 --- a/www/js/obvody.js +++ b/www/js/obvody.js @@ -1,29 +1,24 @@ -// import souboru s obvody: -$.ajax({ - 'async': false, - 'global': false, - 'url': "../../data/geo-obvody.geojson", - 'dataType': "json", - 'success': function (data) { - geojsonFeature = data; +var geojson; +var geojsonUrl = document.getElementById('map').getAttribute('data-geojson'); +$.getJSON(geojsonUrl, function (data) { + geojson = L.geoJSON(data, { + style: style, + filter: picnicFilter, + onEachFeature: onEachFeature + }).addTo(map); + + if (geojson.getLayers().length) { + map.fitBounds(geojson.getBounds(), { padding: [12, 12], animate: false }); } }); -var geojson; -geojson = L.geoJSON(geojsonFeature, { - style: style, - filter: picnicFilter, - onEachFeature: onEachFeature -}).addTo(map); - function picnicFilter(feature) { // obvod ID je z latte obvod_id if (obvod_id == null) return true; if (feature.properties.number == obvod_id) return true } -// center na vybraný obvod: -map.flyToBounds(geojson.getBounds()); +// center na vybraný obvod je v getJSON výše // geolokace: diff --git a/www/js/stari.js b/www/js/stari.js new file mode 100644 index 0000000..b44af1f --- /dev/null +++ b/www/js/stari.js @@ -0,0 +1,631 @@ +(function () { + var mapaEl = document.getElementById('map'); + var dataEl = document.getElementById('stari-data'); + if (!mapaEl || !dataEl || typeof L === 'undefined') { + return; + } + + var stari = JSON.parse(dataEl.textContent || '{}'); + var maxMesicu = parseInt(mapaEl.getAttribute('data-max'), 10) || 24; + var maxPrideleni = parseInt(mapaEl.getAttribute('data-prideleni'), 10) || 12; + var geojsonUrl = mapaEl.getAttribute('data-geojson'); + var obvodId = mapaEl.getAttribute('data-obvod'); + var rezim = mapaEl.getAttribute('data-filtr'); + if (rezim !== 'stari' && rezim !== 'nedokoncene') { + rezim = 'obvody'; + } + + var ZELENA = [27, 158, 62]; + var ZLUTA = [230, 184, 0]; + var CERVENA = [198, 40, 40]; + var SEDA = '#9e9e9e'; + var SEDA_HRANICE = '#616161'; + var MODRA = '#2652a8'; + var MODRA_PLOCHA = '#5c8fd6'; + + function mix(a, b, t) { + return [ + Math.round(a[0] + (b[0] - a[0]) * t), + Math.round(a[1] + (b[1] - a[1]) * t), + Math.round(a[2] + (b[2] - a[2]) * t), + ]; + } + + function rgb(kanaly) { + return 'rgb(' + kanaly[0] + ',' + kanaly[1] + ',' + kanaly[2] + ')'; + } + + function barvaStari(mesice, stupnice) { + var max = stupnice || maxMesicu; + var t = mesice == null ? 1 : Math.max(0, Math.min(mesice / max, 1)); + if (t < 0.5) { + return rgb(mix(ZELENA, ZLUTA, t / 0.5)); + } + return rgb(mix(ZLUTA, CERVENA, (t - 0.5) / 0.5)); + } + + function infoObvodu(feature) { + if (!feature.properties || feature.properties.number == null) { + return null; + } + return stari[String(feature.properties.number)] || null; + } + + function style(feature) { + var info = infoObvodu(feature); + if (rezim === 'obvody') { + return { + fillColor: MODRA_PLOCHA, + color: '#1d4f91', + weight: 2, + opacity: 1, + fillOpacity: 0.4, + }; + } + if (rezim === 'nedokoncene') { + if (info && info.probiha) { + var barvaPrideleni = barvaStari(info.prideleni, maxPrideleni); + return { + fillColor: barvaPrideleni, + color: barvaPrideleni, + weight: 2, + opacity: 1, + fillOpacity: 0.55, + }; + } + return { + fillColor: '#b0bec5', + color: '#90a4ae', + weight: 1, + opacity: 0.4, + dashArray: '3', + fillOpacity: 0.04, + }; + } + var mesice = info ? info.mesice : null; + var barva = barvaStari(mesice); + if (info && info.probiha) { + return { + fillColor: SEDA, + color: SEDA_HRANICE, + weight: 2, + opacity: 1, + fillOpacity: 0.72, + }; + } + return { + fillColor: barva, + color: barva, + weight: 2, + opacity: 1, + fillOpacity: 0.55, + }; + } + + var API_KEY = 'Uw8gm6JPZpnT3GfNnh8adARpQI-WBGy6QaY9g6VhD74'; + var mapyCZ = L.tileLayer('https://api.mapy.cz/v1/maptiles/basic/256/{z}/{x}/{y}?apikey=' + API_KEY, { + minZoom: 0, + maxZoom: 19, + attribution: '© Seznam.cz a.s. a další', + }); + var mapyCZletecka = L.tileLayer('https://api.mapy.cz/v1/maptiles/aerial/256/{z}/{x}/{y}?apikey=' + API_KEY, { + minZoom: 0, + maxZoom: 19, + attribution: '© Seznam.cz a.s. a další', + }); + var osm = L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { + maxZoom: 19, + attribution: '© OpenStreetMap', + }); + + var map = L.map(mapaEl, { + center: [50.853501146, 14.8425], + zoom: 13, + layers: [mapyCZ], + }); + + L.control.layers({ + 'Mapy.cz': mapyCZ, + 'Mapy.cz (letecká)': mapyCZletecka, + 'OpenStreetMap': osm, + }).addTo(map); + + var LogoControl = L.Control.extend({ + options: { position: 'bottomleft' }, + onAdd: function () { + var container = L.DomUtil.create('div'); + var link = L.DomUtil.create('a', '', container); + link.setAttribute('href', 'http://mapy.cz/'); + link.setAttribute('target', '_blank'); + link.innerHTML = 'Mapy.cz'; + L.DomEvent.disableClickPropagation(link); + return container; + }, + }); + new LogoControl().addTo(map); + + var legendaEl = null; + var Legenda = L.Control.extend({ + options: { position: 'bottomright' }, + onAdd: function () { + legendaEl = L.DomUtil.create('div', 'stari-legenda'); + legendaEl.innerHTML = obsahLegendy(); + L.DomEvent.disableClickPropagation(legendaEl); + return legendaEl; + }, + }); + new Legenda().addTo(map); + + var filtrTlacitka = []; + var Filtr = L.Control.extend({ + options: { position: 'topright' }, + onAdd: function () { + var div = L.DomUtil.create('div', 'mapa-filtr'); + div.innerHTML = + '' + + '' + + ''; + L.DomEvent.disableClickPropagation(div); + filtrTlacitka = div.querySelectorAll('button'); + filtrTlacitka.forEach(function (tlacitko) { + tlacitko.addEventListener('click', function () { + nastavRezim(tlacitko.getAttribute('data-rezim')); + }); + }); + return div; + }, + }); + new Filtr().addTo(map); + + function textMesicu(mesice) { + if (mesice == null) { + return '—'; + } + return mesice + ' měs.'; + } + + function textBadge(mesice) { + return mesice == null ? '—' : mesice + ' měs.'; + } + + function barvaTextu(mesice, stupnice) { + var max = stupnice || maxMesicu; + var t = mesice == null ? 1 : Math.max(0, Math.min(mesice / max, 1)); + return t > 0.72 ? '#fff' : '#1a1a1a'; + } + + function htmlKolecko(text, mesice, sede, stupnice) { + var barva = sede ? SEDA : barvaStari(mesice, stupnice); + var okraj = sede ? SEDA_HRANICE : barva; + var textBarva = sede ? '#1a1a1a' : barvaTextu(mesice, stupnice); + return '
' + text + '
'; + } + + function htmlZnacky(cislo, info) { + if (rezim === 'obvody') { + return '
' + cislo + '
'; + } + if (rezim === 'nedokoncene') { + var prideleni = info ? info.prideleni : null; + var barvaPrideleni = barvaStari(prideleni, maxPrideleni); + return '
' + + htmlKolecko(cislo, prideleni, false, maxPrideleni) + + '
' + textBadge(prideleni) + '
' + + '
'; + } + var mesice = info ? info.mesice : null; + var html = '
' + htmlKolecko(cislo, mesice, !!(info && info.probiha)); + if (!info || !info.probiha) { + var barva = barvaStari(mesice); + html += '
' + textBadge(mesice) + '
'; + } + return html + '
'; + } + + function htmlSkupiny(pocet, mesice, sede, stupnice) { + return '
' + + htmlKolecko(pocet + 'x', mesice, sede, stupnice) + + '
'; + } + + function vytvorSkupinu() { + return L.markerClusterGroup({ + showCoverageOnHover: false, + spiderfyOnMaxZoom: true, + zoomToBoundsOnClick: true, + maxClusterRadius: 46, + disableClusteringAtZoom: 17, + iconCreateFunction: function (cluster) { + var pocet = cluster.getChildCount(); + if (rezim === 'obvody') { + return L.divIcon({ + className: 'obvod-skupina', + html: '
' + pocet + 'x
', + iconSize: [44, 44], + iconAnchor: [22, 22], + }); + } + if (rezim === 'nedokoncene') { + var nejPrideleni = 0; + var bezPrideleni = false; + cluster.getAllChildMarkers().forEach(function (marker) { + if (marker.odPrideleni == null) { + bezPrideleni = true; + } else if (marker.odPrideleni > nejPrideleni) { + nejPrideleni = marker.odPrideleni; + } + }); + return L.divIcon({ + className: 'obvod-skupina', + html: htmlSkupiny(pocet, bezPrideleni ? null : nejPrideleni, false, maxPrideleni), + iconSize: [44, 44], + iconAnchor: [22, 22], + }); + } + var nej = 0; + var bezData = false; + var maHotovy = false; + cluster.getAllChildMarkers().forEach(function (marker) { + if (marker.obvodProbiha) { + return; + } + maHotovy = true; + if (marker.obvodMesice == null) { + bezData = true; + } else if (marker.obvodMesice > nej) { + nej = marker.obvodMesice; + } + }); + var sede = !maHotovy; + return L.divIcon({ + className: 'obvod-skupina', + html: htmlSkupiny(pocet, sede || bezData ? null : nej, sede), + iconSize: [44, 44], + iconAnchor: [22, 22], + }); + }, + }); + } + + var znacky = null; + + function popupHtml(cislo, info) { + var html = 'Obvod č. ' + cislo + '
'; + if (!info || info.vraceno == null) { + html += 'Zatím bez propracování'; + } else { + html += 'Naposledy propracováno ' + info.vraceno + ' (' + textMesicu(info.mesice) + ')'; + } + if (info && info.probiha) { + html += '
Přiděleno ' + (info.prideleniDatum || '—') + ' (' + textMesicu(info.prideleni) + ')'; + } + if (info && info.url) { + html += '
Otevřít obvod'; + } + return html; + } + + function jeLatLng(hodnota) { + return hodnota && typeof hodnota.lat === 'number' && typeof hodnota.lng === 'number'; + } + + function prstence(latlngs) { + if (!latlngs || !latlngs.length) { + return []; + } + if (jeLatLng(latlngs[0])) { + return [latlngs.map(function (bod) { return [bod.lng, bod.lat]; })]; + } + if (jeLatLng(latlngs[0][0])) { + return latlngs.map(function (prstenec) { + return prstenec.map(function (bod) { return [bod.lng, bod.lat]; }); + }); + } + var nej = []; + var nejPlocha = -1; + latlngs.forEach(function (polygon) { + var rings = prstence(polygon); + var plocha = rings.length ? Math.abs(plochaPrstence(rings[0])) : 0; + if (plocha > nejPlocha) { + nejPlocha = plocha; + nej = rings; + } + }); + return nej; + } + + function plochaPrstence(prstenec) { + var plocha = 0; + for (var i = 0, j = prstenec.length - 1; i < prstenec.length; j = i++) { + plocha += prstenec[i][0] * prstenec[j][1] - prstenec[j][0] * prstenec[i][1]; + } + return plocha; + } + + function vzdalenostUsecky(px, py, a, b) { + var x = a[0]; + var y = a[1]; + var dx = b[0] - x; + var dy = b[1] - y; + if (dx !== 0 || dy !== 0) { + var t = ((px - x) * dx + (py - y) * dy) / (dx * dx + dy * dy); + if (t > 1) { + x = b[0]; + y = b[1]; + } else if (t > 0) { + x += dx * t; + y += dy * t; + } + } + dx = px - x; + dy = py - y; + return dx * dx + dy * dy; + } + + function vzdalenostPolygonu(x, y, prstenceBodu) { + var uvnitr = false; + var nejmensi = Infinity; + prstenceBodu.forEach(function (prstenec) { + for (var i = 0, j = prstenec.length - 1; i < prstenec.length; j = i++) { + var a = prstenec[i]; + var b = prstenec[j]; + if ((a[1] > y) !== (b[1] > y) && x < ((b[0] - a[0]) * (y - a[1])) / (b[1] - a[1]) + a[0]) { + uvnitr = !uvnitr; + } + nejmensi = Math.min(nejmensi, vzdalenostUsecky(x, y, a, b)); + } + }); + return (uvnitr ? 1 : -1) * Math.sqrt(nejmensi); + } + + function bunka(x, y, h, prstenceBodu) { + var d = vzdalenostPolygonu(x, y, prstenceBodu); + return { x: x, y: y, h: h, d: d, max: d + h * Math.SQRT2 }; + } + + function vlozHaldu(halda, bunkaBodu) { + halda.push(bunkaBodu); + var i = halda.length - 1; + while (i > 0) { + var rodic = (i - 1) >> 1; + if (halda[rodic].max >= halda[i].max) { + break; + } + var tmp = halda[rodic]; + halda[rodic] = halda[i]; + halda[i] = tmp; + i = rodic; + } + } + + function vyberHaldu(halda) { + var vrchol = halda[0]; + var posledni = halda.pop(); + if (halda.length && vrchol !== posledni) { + halda[0] = posledni; + var i = 0; + while (true) { + var levy = i * 2 + 1; + var pravy = levy + 1; + var nej = i; + if (levy < halda.length && halda[levy].max > halda[nej].max) { + nej = levy; + } + if (pravy < halda.length && halda[pravy].max > halda[nej].max) { + nej = pravy; + } + if (nej === i) { + break; + } + var tmp = halda[i]; + halda[i] = halda[nej]; + halda[nej] = tmp; + i = nej; + } + } + return vrchol; + } + + function teziste(prstenceBodu) { + var prstenec = prstenceBodu[0]; + var plocha = 0; + var x = 0; + var y = 0; + for (var i = 0, j = prstenec.length - 1; i < prstenec.length; j = i++) { + var f = prstenec[i][0] * prstenec[j][1] - prstenec[j][0] * prstenec[i][1]; + x += (prstenec[i][0] + prstenec[j][0]) * f; + y += (prstenec[i][1] + prstenec[j][1]) * f; + plocha += f; + } + if (plocha === 0) { + return [prstenec[0][0], prstenec[0][1]]; + } + return [x / (3 * plocha), y / (3 * plocha)]; + } + + function bodUvnitr(prstenceBodu) { + var minX = Infinity; + var minY = Infinity; + var maxX = -Infinity; + var maxY = -Infinity; + prstenceBodu[0].forEach(function (bod) { + minX = Math.min(minX, bod[0]); + minY = Math.min(minY, bod[1]); + maxX = Math.max(maxX, bod[0]); + maxY = Math.max(maxY, bod[1]); + }); + var sirka = maxX - minX; + var vyska = maxY - minY; + var krok = Math.min(sirka, vyska); + if (krok === 0) { + return [minX, minY]; + } + var pul = krok / 2; + var halda = []; + for (var x = minX; x < maxX; x += krok) { + for (var y = minY; y < maxY; y += krok) { + vlozHaldu(halda, bunka(x + pul, y + pul, pul, prstenceBodu)); + } + } + var stred = teziste(prstenceBodu); + var nej = bunka(stred[0], stred[1], 0, prstenceBodu); + var obdelnik = bunka(minX + sirka / 2, minY + vyska / 2, 0, prstenceBodu); + if (obdelnik.d > nej.d) { + nej = obdelnik; + } + var presnost = Math.max(krok / 40, 0.00005); + var pojistka = 0; + while (halda.length && pojistka < 8000) { + var aktualni = vyberHaldu(halda); + pojistka++; + if (aktualni.d > nej.d) { + nej = aktualni; + } + if (aktualni.max - nej.d <= presnost) { + continue; + } + var h = aktualni.h / 2; + vlozHaldu(halda, bunka(aktualni.x - h, aktualni.y - h, h, prstenceBodu)); + vlozHaldu(halda, bunka(aktualni.x + h, aktualni.y - h, h, prstenceBodu)); + vlozHaldu(halda, bunka(aktualni.x - h, aktualni.y + h, h, prstenceBodu)); + vlozHaldu(halda, bunka(aktualni.x + h, aktualni.y + h, h, prstenceBodu)); + } + return [nej.x, nej.y]; + } + + function vnitrniBod(layer) { + var rings = prstence(layer.getLatLngs()); + if (!rings.length) { + return layer.getBounds().getCenter(); + } + var bod = bodUvnitr(rings); + if (vzdalenostPolygonu(bod[0], bod[1], rings) <= 0) { + return layer.getBounds().getCenter(); + } + return L.latLng(bod[1], bod[0]); + } + + function onEachFeature(feature, layer) { + var cislo = feature.properties && feature.properties.number != null ? feature.properties.number : ''; + var info = infoObvodu(feature); + var stred = vnitrniBod(layer); + var nedokonceny = !!(info && info.probiha); + if (rezim !== 'nedokoncene' || nedokonceny) { + var marker = L.marker(stred, { + icon: L.divIcon({ + className: 'obvod-znacka', + html: htmlZnacky(cislo, info), + iconSize: [58, 46], + iconAnchor: [29, 14], + }), + interactive: false, + keyboard: false, + }); + marker.obvodMesice = info ? info.mesice : null; + marker.odPrideleni = info ? info.prideleni : null; + marker.obvodProbiha = nedokonceny; + znacky.addLayer(marker); + } + + layer.bindPopup(popupHtml(cislo, info)); + layer.on({ + mouseover: function (e) { + e.target.setStyle({ weight: 4, fillOpacity: 0.8 }); + e.target.bringToFront(); + }, + mouseout: function (e) { + geojson.resetStyle(e.target); + }, + }); + } + + var geojson = null; + var geoData = null; + + function filtrObvodu(feature) { + if (!obvodId) { + return true; + } + return feature.properties && String(feature.properties.number) === String(obvodId); + } + + function obsahLegendy() { + if (rezim === 'nedokoncene') { + return '
Měsíce od začátku přidělení
' + + '
' + + '
0' + Math.round(maxPrideleni / 2) + '' + maxPrideleni + '+
'; + } + return '
Měsíce od posledního propracování
' + + '
' + + '
0' + Math.round(maxMesicu / 2) + '' + maxMesicu + '+
' + + '
právě se propracovává
'; + } + + function oznacFiltr() { + filtrTlacitka.forEach(function (tlacitko) { + tlacitko.classList.toggle('is-active', tlacitko.getAttribute('data-rezim') === rezim); + }); + if (legendaEl) { + legendaEl.style.display = rezim === 'obvody' ? 'none' : ''; + if (rezim !== 'obvody') { + legendaEl.innerHTML = obsahLegendy(); + } + } + } + + function prekresli(poprve) { + if (geojson) { + map.removeLayer(geojson); + } + if (znacky) { + map.removeLayer(znacky); + } + znacky = vytvorSkupinu(); + geojson = L.geoJSON(geoData, { + style: style, + filter: filtrObvodu, + onEachFeature: onEachFeature, + }).addTo(map); + znacky.addTo(map); + oznacFiltr(); + if (poprve && geojson.getLayers().length) { + map.fitBounds(geojson.getBounds(), { padding: [12, 12], animate: false }); + } + } + + function nastavRezim(novy) { + if (novy !== 'stari' && novy !== 'nedokoncene') { + novy = 'obvody'; + } + if (novy === rezim) { + return; + } + rezim = novy; + prekresli(false); + if (window.history && window.history.replaceState) { + var url = new URL(window.location.href); + if (rezim === 'obvody') { + url.searchParams.delete('filtr'); + } else { + url.searchParams.set('filtr', rezim); + } + window.history.replaceState(null, '', url); + } + } + + $.getJSON(geojsonUrl, function (data) { + geoData = data; + prekresli(true); + }); + + L.control.locate({ + flyTo: true, + strings: { + title: 'Moje poloha', + metersUnit: 'metrů', + feetUnit: 'feet', + popup: 'Nacházíš se v okolí {distance} {unit} od tohoto bodu.', + outsideMapBoundsMsg: 'Zdá se, že jsi mimo oblast mapy.', + }, + }).addTo(map); +})();