database->fetch($sql, $id); } public const SKUPINA_COOKIE = 'obvodSkupina'; /** * Základní informace o obvodech. * @return array */ public function getObvody(?string $skupina = null): array { if ($skupina === null || $skupina === '') { return $this->database->fetchAll('SELECT ' . self::SLOUPCE . ' FROM OBVOD ORDER BY CISLO'); } return $this->database->fetchAll( 'SELECT ' . self::SLOUPCE . ' 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); } /** * Předchozí a další existující obvod podle čísla. * Čísla nemusí jít po sobě. * * @return array{predchozi: int|null, dalsi: int|null} */ public function getSousede(int $id): array { $prazdne = ['predchozi' => null, 'dalsi' => null]; $radky = $this->database->fetchAll('SELECT ID FROM OBVOD ORDER BY CISLO, ID'); $ids = array_map(static fn(Row $radek): int => (int) $radek->ID, $radky); $index = array_search($id, $ids, true); if ($index === false) { return $prazdne; } return [ 'predchozi' => $index > 0 ? $ids[$index - 1] : null, 'dalsi' => $index < count($ids) - 1 ? $ids[$index + 1] : null, ]; } 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] ?? ''; } /** * Hranice obvodů jako GeoJSON FeatureCollection. * GEO v databázi je GeoJSON geometrie (Polygon nebo MultiPolygon). */ public function getGeoJson(): string { $radky = $this->database->fetchAll( "SELECT CISLO, GEO FROM OBVOD WHERE GEO IS NOT NULL AND LTRIM(RTRIM(GEO)) <> '' ORDER BY CISLO", ); $features = []; foreach ($radky as $radek) { $features[] = '{"type":"Feature","properties":{"number":' . (int) $radek->CISLO . '},"geometry":' . $radek->GEO . '}'; } return '{"type":"FeatureCollection","features":[' . implode(',', $features) . ']}'; } /** * Uloží hranici obvodu. GEO je GeoJSON Polygon s jedním prstencem. */ public function ulozHranici(int $id, string $geo): void { if ($this->database->fetchField('SELECT ID FROM OBVOD WHERE ID = ?', $id) === null) { throw new \InvalidArgumentException('Obvod nenalezen.'); } $this->database->query( 'UPDATE OBVOD SET GEO = ? WHERE ID = ?', $this->overHranici($geo), $id, ); } private function overHranici(string $geo): string { try { $data = Json::decode($geo, forceArrays: true); } catch (JsonException) { throw new \InvalidArgumentException('Hranice nejsou platný JSON.'); } if (!is_array($data) || ($data['type'] ?? '') !== 'Polygon' || !isset($data['coordinates']) || !is_array($data['coordinates'])) { throw new \InvalidArgumentException('Hranice musí být polygon.'); } if (count($data['coordinates']) !== 1 || !is_array($data['coordinates'][0])) { throw new \InvalidArgumentException('Polygon musí mít jeden prstenec bez děr.'); } $kruh = []; foreach ($data['coordinates'][0] as $bod) { if (!is_array($bod) || count($bod) < 2 || !is_numeric($bod[0]) || !is_numeric($bod[1])) { throw new \InvalidArgumentException('Bod hranice nemá souřadnice.'); } $lng = round((float) $bod[0], 7); $lat = round((float) $bod[1], 7); if ($lng < 12.0 || $lng > 19.0 || $lat < 48.0 || $lat > 52.0) { throw new \InvalidArgumentException('Bod hranice je mimo oblast obvodů.'); } $kruh[] = [$lng, $lat]; } if (count($kruh) > 1 && $kruh[0] === $kruh[count($kruh) - 1]) { array_pop($kruh); } if (count($kruh) < 3) { throw new \InvalidArgumentException('Hranice musí mít aspoň tři body.'); } if (count($kruh) > 2000) { throw new \InvalidArgumentException('Hranice má příliš mnoho bodů.'); } $kruh[] = $kruh[0]; return Json::encode([ 'type' => 'Polygon', 'coordinates' => [$kruh], ]); } }