Mnoho změn - Zvestovatele zrušeni, Mapy celého obvodu

master
Kowalski 2026-10-11 20:11:38 +02:00
parent a546f95b56
commit fdbaacb357
21 changed files with 1861 additions and 50 deletions

View File

@ -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<int, Row>
*/
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<string>
*/
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] ?? '';
}
}

View File

@ -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<string, string>
*/
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<int, array{id: int, mesice: int|null, probiha: bool, vraceno: string|null, prideleni: int|null, prideleniDatum: string|null}>
*/
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
);
}
}

View File

@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace App\Model;
use Nette\Database\Explorer;
use Nette\Database\Row;
/**
* Jména zvěstovatelů odvozená ze záznamů.
*/
final class ZvestovatelFacade
{
public function __construct(
private Explorer $database,
) {
}
/**
* @return list<Row>
*/
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
);
}
}

View File

@ -28,7 +28,7 @@
</script>
{* naše vlastní styly *}
<link href="{$basePath}/css/base.css" rel="stylesheet">
<link href="{$basePath}/css/base.css?v=3" rel="stylesheet">
<title>{ifset title}{include title|stripHtml} | {/ifset}Obvody</title>
</head>
@ -54,7 +54,8 @@
{if $user->isLoggedIn()}
<a class='nav-link' n:href=':Home:'>Obvody</a>
<a class='nav-link' n:href=':Mapa:'>Celý obvod</a>
<a class='nav-link' n:href=':Obvod:list'>Seznam obvodů</a>
<a class='nav-link' n:href=':Obvod:list'>Obvody</a>
<a class='nav-link' n:href=':Zvestovatel:'>Zvěstovatelé</a>
<a class='nav-link' n:href=":Sign:out">Odhlásit</a>
{else}
<a class='nav-link' n:href=':Sign:in'>Přihlásit</a>

View File

@ -1,7 +1,7 @@
{* This is the welcome page, you can delete it *}
{block content}
<h3>Seznam obvodů</h3>
<h3>Obvody</h3>
{* Tlačítka: *}

View File

@ -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);
}
}

View File

@ -1,11 +1,14 @@
{* Zobrazení obvodu na mapě. *}
{block title}Celý obvod{/block}
{block content}
<div id="map"></div>
<script type="text/javascript">
var obvod_id = {$obvod_id};
</script>
<script src="{$basePath}/js/mapa.js"></script>
<script src="{$basePath}/js/obvody.js"></script>
<div id="map"
data-geojson="{$basePath}/data/geo-obvody.geojson"
data-max="{$maxMesicu}"
data-prideleni="{$maxPrideleni}"
data-filtr="{$filtr}"
data-obvod="{if $obvod_id}{$obvod_id}{/if}"></div>
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.css" />
<script src="https://unpkg.com/leaflet.markercluster@1.5.3/dist/leaflet.markercluster.js"></script>
<script type="application/json" id="stari-data">{$stariJson|noescape}</script>
<script src="{$basePath}/js/stari.js?v=9"></script>
{/block}

View File

@ -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);
}
}

View File

@ -1,7 +1,31 @@
{block content}
{foreach $obvody as $obvod}
<a n:href=":Obvod:view id:$obvod->ID" type="button" class="btn btn-klavesa btn-letter btn-flex box-shadow--6dp">
{$obvod->CISLO}
</a>
{/foreach}
<div class="container-fluid p-2">
<h3>Obvody</h3>
<div class="d-flex flex-wrap gap-2 mb-2" n:if="$skupiny">
<div class="btn-group flex-wrap" role="group" aria-label="Filtr skupin">
<a n:foreach="$skupiny as $s" n:href="list, skupina: $s" n:class="$skupina === $s ? 'btn btn-klavesa' : 'btn btn-outline-secondary'">{$s}</a>
</div>
</div>
<table class="table table-sm table-striped mb-1">
<thead>
<tr>
<th scope="col" class="text-start">Číslo</th>
<th scope="col" class="text-start">Název</th>
<th scope="col" class="text-start">Akce</th>
</tr>
</thead>
<tbody>
{foreach $obvody as $obvod}
<tr>
<td>{$obvod->CISLO}</td>
<td>{$obvod->NAZEV}</td>
<td>
<a n:href=":Obvod:view id:$obvod->ID" class="btn btn-sm btn-klavesa box-shadow--6dp">Otevřít</a>
</td>
</tr>
{/foreach}
</tbody>
</table>
</div>

View File

@ -1,12 +1,18 @@
{block content}
<div class="container-fluid p-2">
Číslo: {$obvod->CISLO}<br>
Název: {$obvod->NAZEV}<br>
Skupina: {$obvod->SKUPINA}<br>
<a n:href=":Mapa: obvod:$obvod->CISLO" type="button" class="btn btn-klavesa btn-flex box-shadow--6dp">Mapa</a>
<div id="obvod-nahled"
class="box-shadow--6dp my-2"
data-obvod="{$obvod->CISLO}"
data-geojson="{$basePath}/data/geo-obvody.geojson"></div>
<h5>Záznamy:</h5>
<a n:href=":Mapa: obvod:$obvod->CISLO" type="button" class="btn btn-klavesa btn-flex box-shadow--6dp">Přidělit</a>
<a n:href=":Mapa: obvod:$obvod->CISLO" type="button" class="btn btn-klavesa btn-flex box-shadow--6dp">Vrátit</a>
<button type="button" class="btn btn-klavesa btn-flex box-shadow--6dp" data-bs-toggle="modal" data-bs-target="#modalPridelit">Přidělit</button>
<button type="button" class="btn btn-klavesa btn-flex box-shadow--6dp" data-bs-toggle="modal" data-bs-target="#modalVratit">Vrátit</button>
<table class="table table-sm table-striped mx-auto mb-1">
<thead>
<tr>
@ -19,14 +25,170 @@ Skupina: {$obvod->SKUPINA}<br>
<tbody>
{foreach $zaznamy as $zaznam}
<tr>
<td>{$zaznam->ZAZNAM}</td>
<td>{$zaznam->JMENO}</td>
<td class="text-start">
{date_format($zaznam->VYDANO,"d.m.Y")}
</td>
<td class="text-start">
{date_format($zaznam->VRACENO,"d.m.Y")}
{if $zaznam->VRACENO}{date_format($zaznam->VRACENO,"d.m.Y")}{/if}
</td>
<td>
<div class="d-flex flex-wrap gap-1">
<button type="button" class="btn btn-sm btn-klavesa box-shadow--6dp" data-bs-toggle="modal" data-bs-target="#modalUpravit" data-id="{$zaznam->ID}" data-jmeno="{$zaznam->JMENO}" data-vydano="{date_format($zaznam->VYDANO, 'Y-m-d')}" data-vraceno="{if $zaznam->VRACENO}{date_format($zaznam->VRACENO, 'Y-m-d')}{/if}">Upravit</button>
{form "smazat-$zaznam->ID", class: 'd-inline'}
<button n:name="send" class="btn btn-sm btn-outline-danger" onclick="return confirm('Smazat záznam?')">Smazat</button>
{/form}
</div>
</td>
</tr>
{/foreach}
</tbody>
</table>
</div>
<div class="modal fade" id="modalPridelit" tabindex="-1" aria-labelledby="modalPridelitLabel" aria-hidden="true" data-show-errors="{if $presenter['pridelitForm']->hasErrors()}1{else}0{/if}">
<div class="modal-dialog">
<div class="modal-content">
<form n:name="pridelitForm">
<div class="modal-header">
<h5 class="modal-title" id="modalPridelitLabel">Přidělit obvod</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Zavřít"></button>
</div>
<div class="modal-body">
<div class="alert alert-danger" n:if="$form->hasErrors()" n:foreach="$form->getErrors() as $error">{$error}</div>
<div class="mb-2">
<label n:name="zvestovatel" class="form-label">Zvěstovatel</label>
<div class="naseptavac" data-naseptavac>
<input n:name="zvestovatel" class="form-control" autocomplete="off" role="combobox" aria-autocomplete="list" aria-expanded="false" aria-controls="naseptavac-nabidka">
<div id="naseptavac-nabidka" class="naseptavac-nabidka" role="listbox" hidden></div>
</div>
<div class="form-text">Začněte psát jméno. Když v seznamu není, uloží se jako nový zvěstovatel.</div>
</div>
<div>
<label n:name="datum" class="form-label">Datum přidělení</label>
<input n:name="datum" class="form-control">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Zrušit</button>
<button n:name="send" class="btn btn-klavesa box-shadow--6dp">Přidělit</button>
</div>
</form>
</div>
</div>
</div>
<div class="modal fade" id="modalVratit" tabindex="-1" aria-labelledby="modalVratitLabel" aria-hidden="true" data-show-errors="{if $presenter['vratitForm']->hasErrors()}1{else}0{/if}">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="modalVratitLabel">Vrátit obvod</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Zavřít"></button>
</div>
{if count($otevrene) === 0}
<div class="modal-body">Tento obvod nemá otevřené přidělení.</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Zavřít</button>
</div>
{else}
<form n:name="vratitForm">
<div class="modal-body">
<div class="alert alert-danger" n:if="$form->hasErrors()" n:foreach="$form->getErrors() as $error">{$error}</div>
<div class="mb-2">
<label n:name="zaznam" class="form-label">Záznam</label>
<select n:name="zaznam" class="form-select">
<option n:foreach="$otevrene as $otevreny" n:name="zaznam:$otevreny->ID">
{$otevreny->JMENO} (přiděleno {date_format($otevreny->VYDANO, 'd.m.Y')})
</option>
</select>
</div>
<div>
<label n:name="datum" class="form-label">Datum vrácení</label>
<input n:name="datum" class="form-control">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Zrušit</button>
<button n:name="send" class="btn btn-klavesa box-shadow--6dp">Vrátit</button>
</div>
</form>
{/if}
</div>
</div>
</div>
<div class="modal fade" id="modalUpravit" tabindex="-1" aria-labelledby="modalUpravitLabel" aria-hidden="true" data-show-errors="{if $presenter['upravitForm']->hasErrors()}1{else}0{/if}">
<div class="modal-dialog">
<div class="modal-content">
<form n:name="upravitForm">
<div class="modal-header">
<h5 class="modal-title" id="modalUpravitLabel">Upravit záznam</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Zavřít"></button>
</div>
<div class="modal-body">
<div class="alert alert-danger" n:if="$form->hasErrors()" n:foreach="$form->getErrors() as $error">{$error}</div>
<input n:name="id">
<div class="mb-2">
<label n:name="zvestovatel" class="form-label">Zvěstovatel</label>
<div class="naseptavac" data-naseptavac>
<input n:name="zvestovatel" class="form-control" autocomplete="off" role="combobox" aria-autocomplete="list" aria-expanded="false" aria-controls="naseptavac-nabidka-upravit">
<div id="naseptavac-nabidka-upravit" class="naseptavac-nabidka" role="listbox" hidden></div>
</div>
</div>
<div class="mb-2">
<label n:name="vydano" class="form-label">Datum přidělení</label>
<input n:name="vydano" class="form-control">
</div>
<div>
<label n:name="vraceno" class="form-label">Datum vrácení</label>
<input n:name="vraceno" class="form-control">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Zrušit</button>
<button n:name="send" class="btn btn-klavesa box-shadow--6dp">Uložit</button>
</div>
</form>
</div>
</div>
</div>
<script type="application/json" id="zvestovatele-data">{$zvestovateleJson|noescape}</script>
<script src="{$basePath}/js/obvod-nahled.js"></script>
<script src="{$basePath}/js/naseptavac.js?v=3"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('.modal[data-show-errors="1"]').forEach(function (el) {
bootstrap.Modal.getOrCreateInstance(el).show();
});
var vratitMin = {
{foreach $otevrene as $otevreny}{$otevreny->ID}: {date_format($otevreny->VYDANO, 'Y-m-d')},{/foreach}
};
var zaznam = document.getElementById('frm-vratitForm-zaznam');
var datum = document.getElementById('frm-vratitForm-datum');
if (!zaznam || !datum) {
return;
}
var nastavMin = function () {
datum.min = vratitMin[zaznam.value] || '';
};
zaznam.addEventListener('change', nastavMin);
nastavMin();
});
var modalUpravit = document.getElementById('modalUpravit');
if (modalUpravit) {
modalUpravit.addEventListener('show.bs.modal', function (event) {
var btn = event.relatedTarget;
var form = modalUpravit.querySelector('form');
if (!btn || !btn.dataset.id || !form) {
return;
}
form.querySelector('[name="id"]').value = btn.dataset.id;
form.querySelector('[name="zvestovatel"]').value = btn.dataset.jmeno || '';
form.querySelector('[name="vydano"]').value = btn.dataset.vydano || '';
form.querySelector('[name="vraceno"]').value = btn.dataset.vraceno || '';
});
}
</script>

View File

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace App\Presentation\Stari;
use Nette;
final class StariPresenter extends Nette\Application\UI\Presenter
{
public function actionDefault(): void
{
$this->redirect(':Mapa:default', ['filtr' => 'stari']);
}
}

View File

@ -0,0 +1,9 @@
{block title}Stáří obvodů{/block}
{block content}
<div id="map" data-geojson="{$basePath}/data/geo-obvody.geojson" data-max="{$maxMesicu}"></div>
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.css" />
<script src="https://unpkg.com/leaflet.markercluster@1.5.3/dist/leaflet.markercluster.js"></script>
<script type="application/json" id="stari-data">{$stariJson|noescape}</script>
<script src="{$basePath}/js/stari.js?v=5"></script>
{/block}

View File

@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace App\Presentation\Zvestovatel;
use App\Model\ZvestovatelFacade;
use Nette;
use Nette\Application\UI\Form;
use Nette\Utils\ArrayHash;
final class ZvestovatelPresenter extends Nette\Application\UI\Presenter
{
public function __construct(
private ZvestovatelFacade $zvestovatele,
) {
}
public function renderDefault(): void
{
$this->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');
}
}

View File

@ -0,0 +1,26 @@
{block content}
<div class="container-fluid p-2">
<h3>Zvěstovatelé</h3>
<table class="table table-sm table-striped mb-1">
<thead>
<tr>
<th scope="col" class="text-start">Jméno</th>
<th scope="col" class="text-start">Záznamy</th>
<th scope="col" class="text-start">Akce</th>
</tr>
</thead>
<tbody>
{foreach $zvestovatele as $zvestovatel}
<tr>
<td>{$zvestovatel->JMENO}</td>
<td>{$zvestovatel->POCET}</td>
<td>
<a n:href="edit, jmeno: $zvestovatel->JMENO" class="btn btn-sm btn-klavesa box-shadow--6dp">Upravit</a>
</td>
</tr>
{/foreach}
</tbody>
</table>
</div>

View File

@ -0,0 +1,15 @@
{block content}
<div class="container-fluid p-2">
<h3>Upravit zvěstovatele</h3>
<form n:name="zvestovatelForm" class="mb-3">
<input n:name="puvodni">
<div class="alert alert-danger" n:if="$form->hasErrors()" n:foreach="$form->getErrors() as $error">{$error}</div>
<div class="mb-2">
<label n:name="jmeno" class="form-label">Jméno</label>
<input n:name="jmeno" class="form-control">
</div>
<button n:name="send" class="btn btn-klavesa box-shadow--6dp">Uložit</button>
<a n:href="default" class="btn btn-secondary">Zpět</a>
</form>
</div>

View File

@ -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) {

View File

@ -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ů:

116
www/js/naseptavac.js Normal file
View File

@ -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();
}
});
});
})();

66
www/js/obvod-nahled.js Normal file
View File

@ -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: '<a href="https://api.mapy.cz/copyright" target="_blank">&copy; Seznam.cz a.s. a další</a>',
});
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 = '<img src="https://api.mapy.cz/img/api/logo.svg" alt="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();
});
})();

View File

@ -1,20 +1,16 @@
// import souboru s obvody:
$.ajax({
'async': false,
'global': false,
'url': "../../data/geo-obvody.geojson",
'dataType': "json",
'success': function (data) {
geojsonFeature = data;
}
});
var geojson;
geojson = L.geoJSON(geojsonFeature, {
var geojsonUrl = document.getElementById('map').getAttribute('data-geojson');
$.getJSON(geojsonUrl, function (data) {
geojson = L.geoJSON(data, {
style: style,
filter: picnicFilter,
onEachFeature: onEachFeature
}).addTo(map);
}).addTo(map);
if (geojson.getLayers().length) {
map.fitBounds(geojson.getBounds(), { padding: [12, 12], animate: false });
}
});
function picnicFilter(feature) {
// obvod ID je z latte obvod_id
@ -22,8 +18,7 @@ function picnicFilter(feature) {
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:

631
www/js/stari.js Normal file
View File

@ -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: '<a href="https://api.mapy.cz/copyright" target="_blank">&copy; Seznam.cz a.s. a další</a>',
});
var mapyCZletecka = L.tileLayer('https://api.mapy.cz/v1/maptiles/aerial/256/{z}/{x}/{y}?apikey=' + API_KEY, {
minZoom: 0,
maxZoom: 19,
attribution: '<a href="https://api.mapy.cz/copyright" target="_blank">&copy; Seznam.cz a.s. a další</a>',
});
var osm = L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>',
});
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 = '<img src="https://api.mapy.cz/img/api/logo.svg" alt="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 =
'<button type="button" data-rezim="obvody">Obvody</button>' +
'<button type="button" data-rezim="stari">Nepropracované</button>' +
'<button type="button" data-rezim="nedokoncene">Rozpracované</button>';
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 '<div class="obvod-kolecko" style="background:' + barva + ';border-color:' + okraj + ';color:' + textBarva + '">' + text + '</div>';
}
function htmlZnacky(cislo, info) {
if (rezim === 'obvody') {
return '<div class="obvod-znacka-telo"><div class="obvod-kolecko" style="background:' + MODRA + ';border-color:#1d4f91;color:#fff">' + cislo + '</div></div>';
}
if (rezim === 'nedokoncene') {
var prideleni = info ? info.prideleni : null;
var barvaPrideleni = barvaStari(prideleni, maxPrideleni);
return '<div class="obvod-znacka-telo">' +
htmlKolecko(cislo, prideleni, false, maxPrideleni) +
'<div class="obvod-badge" style="background:' + barvaPrideleni + ';color:' + barvaTextu(prideleni, maxPrideleni) + '">' + textBadge(prideleni) + '</div>' +
'</div>';
}
var mesice = info ? info.mesice : null;
var html = '<div class="obvod-znacka-telo">' + htmlKolecko(cislo, mesice, !!(info && info.probiha));
if (!info || !info.probiha) {
var barva = barvaStari(mesice);
html += '<div class="obvod-badge" style="background:' + barva + ';color:' + barvaTextu(mesice) + '">' + textBadge(mesice) + '</div>';
}
return html + '</div>';
}
function htmlSkupiny(pocet, mesice, sede, stupnice) {
return '<div class="obvod-skupina-telo" title="' + pocet + ' obvodů">' +
htmlKolecko(pocet + 'x', mesice, sede, stupnice) +
'</div>';
}
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: '<div class="obvod-skupina-telo"><div class="obvod-kolecko" style="background:#1b9e3e;border-color:#146b2b;color:#fff">' + pocet + 'x</div></div>',
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 = '<strong>Obvod č. ' + cislo + '</strong><br>';
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 += '<br>Přiděleno ' + (info.prideleniDatum || '—') + ' (' + textMesicu(info.prideleni) + ')';
}
if (info && info.url) {
html += '<br><a href="' + info.url + '">Otevřít obvod</a>';
}
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 '<div class="stari-legenda-nadpis">Měsíce od začátku přidělení</div>' +
'<div class="stari-legenda-skala"></div>' +
'<div class="stari-legenda-popisky"><span>0</span><span>' + Math.round(maxPrideleni / 2) + '</span><span>' + maxPrideleni + '+</span></div>';
}
return '<div class="stari-legenda-nadpis">Měsíce od posledního propracování</div>' +
'<div class="stari-legenda-skala"></div>' +
'<div class="stari-legenda-popisky"><span>0</span><span>' + Math.round(maxMesicu / 2) + '</span><span>' + maxMesicu + '+</span></div>' +
'<div class="stari-legenda-seda"><span></span> právě se propracovává</div>';
}
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);
})();