This commit is contained in:
Dauren777 2026-06-24 07:50:00 +00:00
parent 1d90f1d0cc
commit 8b031562ba
5 changed files with 1082 additions and 0 deletions

173
AGENTS.md Normal file
View File

@ -0,0 +1,173 @@
<!-- vibe42-agents-version: v3-guided-2026-06-01 -->
# Vibe42 — учебная песочница для лендингов
Workspace юзера `Ermek_aubakirov`. Это **учебная среда**, где обычные люди (не разработчики) пробуют сделать свой первый сайт.
---
## 🎯 ТВОЯ РОЛЬ
Ты — **гид и помощник**, а не слепой исполнитель. Цель сессии — чтобы юзер вышел с:
1. **рабочим лендингом**, опубликованным по адресу `https://pages.git.vibe42.kz/Ermek_aubakirov/<repo>/`,
2. ощущением «это было легко» — без серверов, БД, токенов, конфигов.
Юзер не разработчик. Ему важен **результат, который видно в браузере**, а не код.
---
## 🗺 СЦЕНАРИЙ ПЕРВОГО ЗАХОДА (юзер только зашёл, ещё ничего нет)
1. Поздоровайся коротко: «Привет! Тут за 10 минут собираем лендинг и публикуем его в интернете. О чём хочешь сделать?»
2. Если он не знает — предложи **4 конкретных идеи** (выбирай близкие к нему, не абстрактные):
- Промо хобби (фотография / музыка / спорт)
- Резюме / personal page с контактами
- Афиша мероприятия (концерт, день рождения, мастер-класс)
- Меню заведения / прайс услуг
- Лендинг продукта или будущего проекта (waitlist)
3. Уточни **2 короткие детали**: стиль (тёмный/светлый/яркий) и главную цель (рассказать / собрать заявку / показать работы).
4. Сразу делай `./new-project <name>` и собирай страницу. Не спрашивай разрешения на каждый шаг.
---
## 💬 ЕСЛИ ЮЗЕР ОТВЕЧАЕТ РАСПЛЫВЧАТО
Юзер говорит «сделай что-нибудь» / «ну хз» / «сюрприз» → **не делай ничего абстрактного**.
Скажи: «Давай определимся, я задам 3 коротких вопроса:
1. Это для тебя лично, для проекта/бизнеса, или для события?
2. Главная цель — рассказать о чём-то / собрать заявку / показать портфолио?
3. Любимое настроение — строгое тёмное, лёгкое светлое, яркое цветное?»
После ответов **сразу** предложи 2 конкретных варианта названия+структуры. Дай выбрать и иди делать.
---
## 🚫 ЕСЛИ ЮЗЕР ХОЧЕТ СЛОЖНОЕ — ПЕРЕФОРМУЛИРУЙ В ЛЕНДИНГ
| Запрос | Что делаем вместо |
|--------|-------------------|
| «магазин с корзиной» | лендинг с товарами + кнопка «купить» = ссылка на WhatsApp / Telegram |
| «соцсеть» | лендинг будущего проекта + waitlist-форма (Formspree / Getform) |
| «блог с админкой» | personal-page + ссылки на статьи в Telegram/Medium |
| «приложение для записи» | лендинг услуги + ссылка на Calendly / WhatsApp |
| «сайт с входом юзеров» | публичный лендинг без логина (нам логин не нужен) |
| «бот в Telegram» | лендинг с описанием бота + кнопка `t.me/...` |
**Не говори «это невозможно».** Скажи: «У нас песочница только для статических сайтов. Давай сделаем лендинг, который покажет твою идею — а кнопки/формы свяжем с готовыми сервисами (WhatsApp, Telegram, Formspree)». Юзер счастлив, результат за 15 минут.
---
## 📐 ШАБЛОНЫ СТРАНИЦ (выбирай под идею юзера)
### A — Промо продукта/услуги
**Секции:** Hero (заголовок + подзаголовок + CTA-кнопка) → 3-4 преимущества (иконка emoji + текст) → социальное доказательство (отзыв или цифра) → CTA (кнопка/телефон/мессенджер).
### B — Personal / резюме
**Секции:** Hero (фото-аватарка + имя + одна фраза «кто я») → О себе (1-2 абзаца) → 3-5 карточек проектов/опыта → Контакты (email, telegram, github как ссылки-кнопки).
### C — Афиша мероприятия
**Секции:** Hero (название + дата + место крупно) → Программа (список с временем) → Локация (картинка-placeholder + адрес) → Регистрация (форма Formspree или контакт).
### D — Меню / прайс
**Секции:** Hero (название + слоган) → Меню/прайс (категории с ценами) → Контакты (телефон, адрес, часы работы, карта-картинка).
### E — Waitlist для будущего проекта
**Секции:** Hero (название проекта + одна фраза + email-форма) → 3 фичи «что будет» → FAQ (3 пункта) → CTA (та же email-форма).
Все шаблоны — **одна страница, прокрутка вниз**. Никаких роутов, ничего динамического.
---
## ⚡ РИТУАЛ ПОСЛЕ ПЕРВОГО ЗАПУСКА
Как только готов первый рабочий вариант (даже грубый):
1. **Сразу запушь:**
```bash
git add -A
git commit -m "v1"
git push origin HEAD:pages
```
2. **ОБЯЗАТЕЛЬНО** дай юзеру ссылку **жирно**:
> 🎉 Готово! Твой лендинг здесь: **https://pages.git.vibe42.kz/Ermek_aubakirov/<repo>/**
3. Скажи: «Открой в новой вкладке, посмотри. Что хочешь поменять?»
4. Дальше короткие итерации: правка → push → новый URL-показ. Каждые 2-3 правки — push.
---
## ⚠️ ЖЕЛЕЗНЫЕ ПРАВИЛА (НЕ нарушать никогда)
1. **Только статика — HTML + CSS + JS в браузере.**
2. **Никакого бэкенда.** Никаких Node/Express/FastAPI/Django/PHP/Go-серверов. Никаких БД. Никакого Redis.
3. **Никакой аутентификации / OAuth / JWT.**
4. **Никакого Docker, nginx, sudo, системных настроек.**
5. **Никаких тяжёлых сборщиков** (`npm install` дерево на 500МБ). Tailwind — только через CDN.
6. **НИКОГДА `git init` в workspace root (`/workspaces/Ermek_aubakirov`)** — это папка-контейнер юзера, не репозиторий.
---
## ✅ ВСЕГДА работай через `./new-project`
Если юзер сказал «сделай сайт NAME» / «создай проект NAME»:
```bash
cd /workspaces/Ermek_aubakirov
./new-project NAME # создаёт repo в Gitea + клонит локально в ./NAME/
cd NAME
# теперь создавай index.html / style.css / script.js внутри ./NAME
```
`./new-project` сам создаёт repo, клонит, и копирует туда `AGENTS.md` + `design.md`.
---
## 🌐 Git и публикация
**НЕТ GitHub.** Self-hosted git: **https://git.vibe42.kz**
- Профиль юзера: https://git.vibe42.kz/Ermek_aubakirov
- Pages (живые лендинги): https://pages.git.vibe42.kz/Ermek_aubakirov/<repo>/
- Креды уже в `/workspaces/Ermek_aubakirov/.git-credentials` — git push/clone работают без пароля
- **НЕ спрашивай юзера про GitHub URL / токен** — их не нужно
### Опубликовать лендинг
```bash
git add -A
git commit -m "site"
git push origin HEAD:pages
```
Ветка **`pages`** (Caddy её обслуживает; `gh-pages` тоже работает как fallback). Push → лендинг доступен мгновенно.
---
## 🔧 Когда что-то идёт не так
- **Pages 404** → запушь ветку `pages` снова: `git push origin HEAD:pages -f`
- **Не дёргай Gitea API типа `/repos/.../pages`, `/settings/pages`, `/deploy_keys`** — их нет
- **Не пытайся «настроить Pages через UI Gitea»** — Pages у нас работают только через push в ветку `pages`
- Запуталось — сделай новый чистый проект через `./new-project NAME-v2`, перенеси туда работающий index.html
---
## ❌ Чего НЕ делать НИКОГДА
- ❌ `git init` в workspace root
- ❌ `npm install` с прод-зависимостями (express/mongoose/pg/prisma/next/nuxt)
- ❌ Создавать `server.js` / `app.py` / `main.go` как backend
- ❌ Использовать `gh` CLI или GitHub API
- ❌ Вызывать Gitea Pages-API (его нет)
- ❌ Долгое отлаживание Pages — почти всегда решение «push HEAD:pages»
- ❌ Просить юзера ввести токен/URL/пароль — всё уже настроено
- ❌ Задавать юзеру 10 вопросов подряд (максимум 2-3 за раз)
- ❌ Показывать юзеру голый код больше 1 раза — ему важен результат, а не как написано
- ❌ Предлагать «давай сначала дизайн в Figma» — мы делаем сразу в HTML
- ❌ Говорить «это сложно» — переформулируй в простое
- ❌ Зависать в обсуждениях — сделай первый вариант грубо, потом итерируй
---
## 🎨 design.md
Рядом лежит `design.md` с готовой палитрой, типографикой и стартер-шаблоном `index.html`. **Начинай с него.** Не выдумывай новые цвета — модифицируй существующие.

110
design.md Normal file
View File

@ -0,0 +1,110 @@
<!-- vibe42-design-version: v1-2026-06-01 -->
# Design system — Vibe42 песочница
Базовые цвета и типографика для лендингов. Можно отклоняться, но начинай с этого.
## Палитра
| Token | Hex | Использование |
|-------|-----|---------------|
| `--ink` | `#0F1218` | Тёмный фон / основной текст |
| `--cyan` | `#00E5FF` | Основной акцент (кнопки, лого) |
| `--cyan-50` | `#E8FCFF` | Светлая подложка для акцентов |
| `--white` | `#FFFFFF` | Основной фон |
| `--gray-500` | `#5B6573` | Вторичный текст |
| `--gray-100` | `#F2F4F7` | Сепараторы / тонкие фоны |
## Типографика
```css
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, system-ui, sans-serif;
```
| Уровень | Размер | Вес | line-height |
|---------|--------|-----|-------------|
| h1 (hero) | 56px | 800 | 1.05 |
| h2 (section) | 36px | 700 | 1.15 |
| h3 | 22px | 700 | 1.3 |
| body | 17px | 400 | 1.6 |
| small | 14px | 400 | 1.5 |
На мобиле — h1 уменьши до 36px, h2 до 28px.
## Лейаут
- max-width контента: **1140px** (контейнер с padding по бокам)
- секция: `padding: 80px 24px` (мобила: `48px 20px`)
- gap между блоками внутри секции: `24-32px`
- border-radius: `8px` (кнопки, карточки), `16px` (большие карточки)
## Кнопки
```css
.btn-primary {
background: var(--cyan); color: var(--ink);
padding: 14px 28px; border-radius: 8px;
font-weight: 700; text-decoration: none;
display: inline-block;
}
.btn-secondary {
background: transparent; color: var(--ink);
border: 2px solid var(--ink);
padding: 12px 26px; border-radius: 8px;
}
```
## Стартер `index.html`
```html
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Мой проект</title>
<style>
:root{--ink:#0F1218;--cyan:#00E5FF;--cyan-50:#E8FCFF;--white:#fff;--gray-500:#5B6573;--gray-100:#F2F4F7}
*{box-sizing:border-box;margin:0;padding:0}
body{font:17px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Inter,system-ui,sans-serif;color:var(--ink);background:var(--white)}
.container{max-width:1140px;margin:0 auto;padding:80px 24px}
.hero{background:var(--ink);color:var(--white)}
.hero h1{font-size:56px;font-weight:800;line-height:1.05;margin-bottom:24px}
.hero p{font-size:20px;color:#9aa3b2;max-width:600px;margin-bottom:32px}
.btn{display:inline-block;background:var(--cyan);color:var(--ink);padding:14px 28px;border-radius:8px;font-weight:700;text-decoration:none}
.btn:hover{background:#1be5ff}
.section h2{font-size:36px;font-weight:700;margin-bottom:24px}
.card{background:var(--gray-100);border-radius:16px;padding:32px;margin-bottom:16px}
@media (max-width:640px){.hero h1{font-size:36px}.section h2{font-size:28px}.container{padding:48px 20px}}
</style>
</head>
<body>
<section class="hero">
<div class="container">
<h1>Заголовок проекта</h1>
<p>Подзаголовок — пара предложений о чём это.</p>
<a class="btn" href="#section">Начать</a>
</div>
</section>
<section id="section" class="section">
<div class="container">
<h2>Секция</h2>
<div class="card">Контент карточки.</div>
<div class="card">Контент карточки.</div>
</div>
</section>
</body>
</html>
```
## Чем НЕ пользоваться
- Bootstrap, Material UI, Chakra, Ant Design — слишком тяжело и не нужно для лендинга
- Font Awesome — используй emoji (🚀 ⚡ ✨) или inline SVG
- jQuery — vanilla JS более чем достаточно
## Чем МОЖНО (если очень надо)
- **Tailwind через CDN**: `<script src="https://cdn.tailwindcss.com"></script>` — для прототипа OK
- **Lottie animations через CDN**
- **Placeholder картинки**: `https://picsum.photos/800/600`, `https://placehold.co/600x400`
- **Шрифты Google Fonts через `<link>`** в head

126
index.html Normal file
View File

@ -0,0 +1,126 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>PoleMap — расстановка опор</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://cdn.tailwindcss.com"></script>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Inter,system-ui,sans-serif;background:#f1f5f9;color:#0f172a}
#map{height:100%;width:100%;border-radius:12px;min-height:500px}
.pole-badge{display:inline-block;padding:2px 10px;border-radius:20px;color:#fff;font-size:12px;font-weight:600}
.pole-type-btn{padding:8px 14px;border-radius:8px;border:2px solid #e2e8f0;cursor:pointer;font-size:13px;font-weight:500;transition:all .15s;background:#fff}
.pole-type-btn:hover{border-color:#94a3b8}
.pole-type-btn.active{border-color:#0f172a;background:#0f172a;color:#fff}
.pole-type-btn .dot{display:inline-block;width:10px;height:10px;border-radius:50%;margin-right:6px;vertical-align:middle}
#searchResults{position:absolute;top:100%;left:0;right:0;z-index:1000;background:#fff;border:1px solid #e2e8f0;border-radius:8px;max-height:240px;overflow-y:auto;box-shadow:0 4px 12px rgba(0,0,0,.1)}
#searchResults li{font-size:14px;padding:8px 14px}
#searchResults li:hover{background:#f0f9ff}
.btn-primary{background:#0f172a;color:#fff;padding:10px 24px;border-radius:8px;font-weight:600;border:none;cursor:pointer;transition:all .15s;font-size:14px}
.btn-primary:hover{background:#1e293b}
.btn-secondary{background:transparent;color:#0f172a;padding:9px 22px;border-radius:8px;font-weight:500;border:2px solid #cbd5e1;cursor:pointer;transition:all .15s;font-size:14px}
.btn-secondary:hover{border-color:#94a3b8}
input,select{width:100%;padding:10px 14px;border:1px solid #e2e8f0;border-radius:8px;font-size:14px;background:#fff;outline:none}
input:focus,select:focus{border-color:#0f172a}
table{width:100%;border-collapse:collapse;font-size:14px}
table th{text-align:left;padding:10px 16px;font-size:12px;text-transform:uppercase;letter-spacing:.05em;color:#64748b;border-bottom:2px solid #e2e8f0}
table td{padding:10px 16px;border-bottom:1px solid #f1f5f9}
.scrollbar-thin::-webkit-scrollbar{width:6px}
.scrollbar-thin::-webkit-scrollbar-track{background:transparent}
.scrollbar-thin::-webkit-scrollbar-thumb{background:#cbd5e1;border-radius:3px}
.leaflet-popup-content{font-size:13px;line-height:1.5}
</style>
</head>
<body>
<div class="flex h-screen overflow-hidden">
<!-- Sidebar -->
<div class="w-[420px] min-w-[420px] bg-white border-r border-gray-200 flex flex-col overflow-hidden">
<div class="px-5 py-4 border-b border-gray-200">
<h1 class="text-xl font-bold">PoleMap</h1>
<p class="text-sm text-gray-500 mt-1">Расстановка опор связи на карте</p>
</div>
<div class="flex-1 overflow-y-auto scrollbar-thin px-5 py-4 space-y-5">
<!-- Search -->
<div class="relative">
<label class="text-xs font-semibold uppercase tracking-wider text-gray-500 mb-1.5 block">Поиск села</label>
<input id="searchInput" type="text" placeholder="Начните вводить название..." autocomplete="off">
<ul id="searchResults"></ul>
</div>
<!-- Selectors -->
<div class="space-y-3">
<div>
<label class="text-xs font-semibold uppercase tracking-wider text-gray-500 mb-1.5 block">Область</label>
<select id="oblast"><option value="">— Выберите область —</option></select>
</div>
<div>
<label class="text-xs font-semibold uppercase tracking-wider text-gray-500 mb-1.5 block">Район</label>
<select id="rayon"><option value="">— Выберите район —</option></select>
</div>
<div>
<label class="text-xs font-semibold uppercase tracking-wider text-gray-500 mb-1.5 block">Село</label>
<select id="village"><option value="">— Выберите село —</option></select>
</div>
</div>
<!-- Pole types -->
<div>
<label class="text-xs font-semibold uppercase tracking-wider text-gray-500 mb-2 block">Тип опоры</label>
<div class="grid grid-cols-2 gap-2">
<button class="pole-type-btn" data-type="1"><span class="dot" style="background:#2563eb"></span>Промежуточная</button>
<button class="pole-type-btn" data-type="2"><span class="dot" style="background:#dc2626"></span>Угловая</button>
<button class="pole-type-btn" data-type="3"><span class="dot" style="background:#ca8a04"></span>С муфтой</button>
<button class="pole-type-btn" data-type="4"><span class="dot" style="background:#16a34a"></span>С ОРКсп</button>
<button class="pole-type-btn" data-type="5"><span class="dot" style="background:#7c3aed"></span>С муфтой и ОРКсп</button>
</div>
<p class="text-xs text-gray-400 mt-2">Кликните на карту, чтобы поставить опору выбранного типа</p>
</div>
<!-- Actions -->
<div class="flex gap-2">
<button class="btn-primary flex-1" onclick="exportSVG()">📥 SVG</button>
<button class="btn-secondary flex-1" onclick="exportVSD()">📥 VSD</button>
<button class="btn-secondary px-3" onclick="clearAll()" title="Очистить всё">🗑</button>
</div>
</div>
</div>
<!-- Map + Table -->
<div class="flex-1 flex flex-col overflow-hidden">
<div class="flex-1 p-3">
<div id="map"></div>
</div>
<div class="h-64 bg-white border-t border-gray-200 overflow-y-auto scrollbar-thin">
<div class="px-5 py-3 flex items-center justify-between border-b border-gray-100">
<span class="font-semibold text-sm">Опоры (<span id="poleCount">0</span>)</span>
<span class="text-xs text-gray-400">Нумерация по порядку установки</span>
</div>
<table id="poleTable">
<thead>
<tr>
<th class="w-14"></th>
<th>Тип</th>
<th>Широта</th>
<th>Долгота</th>
<th class="text-right">Дист. до пред.</th>
<th class="w-10"></th>
</tr>
</thead>
<tbody>
<tr><td colspan="6" class="text-center text-gray-500 py-4">Кликните на карту, чтобы добавить опору</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="js/kz-data.js"></script>
<script src="js/app.js"></script>
</body>
</html>

379
js/app.js Normal file
View File

@ -0,0 +1,379 @@
let map, drawnItems, poleLayer, connectionLayer;
let poles = [];
let poleCounter = 1;
let currentPoleType = "1";
const POLE_TYPES = {
"1": { name: "Промежуточная", color: "#2563eb", icon: "●" },
"2": { name: "Угловая", color: "#dc2626", icon: "◆" },
"3": { name: "С муфтой", color: "#ca8a04", icon: "◎" },
"4": { name: "С ОРКсп", color: "#16a34a", icon: "◇" },
"5": { name: "С муфтой и ОРКсп", color: "#7c3aed", icon: "★" },
};
function initApp() {
initMap();
initSelectors();
initPoleControls();
updateTable();
}
function initMap() {
map = L.map("map", { zoomControl: true }).setView([48.0, 68.0], 6);
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution: "&copy; OpenStreetMap",
maxZoom: 19,
}).addTo(map);
drawnItems = L.featureGroup().addTo(map);
poleLayer = L.featureGroup().addTo(map);
connectionLayer = L.featureGroup().addTo(map);
map.on("click", function (e) {
addPole(e.latlng.lat, e.latlng.lng);
});
}
function initSelectors() {
const oblastSel = document.getElementById("oblast");
const rayonSel = document.getElementById("rayon");
const villageSel = document.getElementById("village");
const searchInput = document.getElementById("searchInput");
KZ_DATA.forEach((o) => {
const opt = document.createElement("option");
opt.value = o.oblast;
opt.textContent = o.oblast;
oblastSel.appendChild(opt);
});
function updateRayons() {
rayonSel.innerHTML = '<option value="">— Выберите район —</option>';
villageSel.innerHTML = '<option value="">— Выберите село —</option>';
const oblast = KZ_DATA.find((o) => o.oblast === oblastSel.value);
if (!oblast) return;
oblast.rayons.forEach((r) => {
const opt = document.createElement("option");
opt.value = r.rayon;
opt.textContent = r.rayon;
rayonSel.appendChild(opt);
});
updateVillages();
}
function updateVillages() {
villageSel.innerHTML = '<option value="">— Выберите село —</option>';
const oblast = KZ_DATA.find((o) => o.oblast === oblastSel.value);
if (!oblast) return;
const rayon = oblast.rayons.find((r) => r.rayon === rayonSel.value);
if (!rayon) return;
rayon.villages.forEach((v) => {
const opt = document.createElement("option");
opt.value = v;
opt.textContent = v;
villageSel.appendChild(opt);
});
}
oblastSel.addEventListener("change", updateRayons);
rayonSel.addEventListener("change", updateVillages);
villageSel.addEventListener("change", function () {
const name = this.value;
if (!name) return;
const oblast = KZ_DATA.find((o) => o.oblast === oblastSel.value);
if (!oblast) return;
map.setView(oblast.center, 10);
L.popup()
.setLatLng(oblast.center)
.setContent(`<b>${name}</b><br>${oblast.oblast}, ${rayonSel.value}`)
.openOn(map);
});
searchInput.addEventListener("input", function () {
const q = this.value.toLowerCase().trim();
if (q.length < 2) return;
const results = [];
KZ_DATA.forEach((o) => {
o.rayons.forEach((r) => {
r.villages.forEach((v) => {
if (v.toLowerCase().includes(q)) {
results.push({ village: v, rayon: r.rayon, oblast: o.oblast, center: o.center });
}
});
});
});
const list = document.getElementById("searchResults");
list.innerHTML = "";
if (results.length === 0 || results.length > 50) {
if (results.length > 50) list.innerHTML = "<li class='px-3 py-2 text-gray-500'>Слишком много результатов, уточните</li>";
return;
}
results.slice(0, 20).forEach((r) => {
const li = document.createElement("li");
li.className = "px-3 py-2 cursor-pointer hover:bg-cyan-50";
li.textContent = `${r.village}${r.rayon}, ${r.oblast}`;
li.addEventListener("click", () => {
oblastSel.value = r.oblast;
updateRayons();
rayonSel.value = r.rayon;
updateVillages();
villageSel.value = r.village;
map.setView(r.center, 12);
L.popup()
.setLatLng(r.center)
.setContent(`<b>${r.village}</b><br>${r.oblast}, ${r.rayon}`)
.openOn(map);
list.innerHTML = "";
searchInput.value = r.village;
});
list.appendChild(li);
});
});
document.addEventListener("click", function (e) {
if (!e.target.closest("#searchResults") && !e.target.closest("#searchInput")) {
document.getElementById("searchResults").innerHTML = "";
}
});
}
function initPoleControls() {
document.querySelectorAll(".pole-type-btn").forEach((btn) => {
btn.addEventListener("click", function () {
document.querySelectorAll(".pole-type-btn").forEach((b) => b.classList.remove("active"));
this.classList.add("active");
currentPoleType = this.dataset.type;
});
});
document.querySelector('[data-type="1"]').classList.add("active");
}
function addPole(lat, lng) {
const type = POLE_TYPES[currentPoleType];
const id = poleCounter++;
const pole = { id, lat, lng, type: currentPoleType, label: `Опора ${id}` };
poles.push(pole);
renderPole(pole);
updateTable();
updateConnections();
}
function renderPole(pole) {
const type = POLE_TYPES[pole.type];
const icon = L.divIcon({
className: "pole-marker",
html: `<div style="
width:28px;height:28px;border-radius:50%;
background:${type.color};color:#fff;
display:flex;align-items:center;justify-content:center;
font-size:14px;font-weight:700;border:2px solid #fff;
box-shadow:0 2px 6px rgba(0,0,0,.3);
cursor:pointer;
" title="${pole.label} (${type.name})">${pole.id}</div>`,
iconSize: [28, 28],
iconAnchor: [14, 14],
});
const marker = L.marker([pole.lat, pole.lng], { icon }).addTo(poleLayer);
marker.bindPopup(`
<b>${pole.label}</b><br>
Тип: ${type.name}<br>
Координаты: ${pole.lat.toFixed(6)}, ${pole.lng.toFixed(6)}
<br><button onclick="removePole(${pole.id})" style="color:red;cursor:pointer;border:none;background:none;margin-top:4px"> Удалить</button>
`);
pole._marker = marker;
}
function removePole(id) {
const idx = poles.findIndex((p) => p.id === id);
if (idx === -1) return;
const pole = poles[idx];
if (pole._marker) poleLayer.removeLayer(pole._marker);
poles.splice(idx, 1);
poles.forEach((p, i) => {
p.id = i + 1;
p.label = `Опора ${i + 1}`;
});
poleCounter = poles.length + 1;
poleLayer.clearLayers();
poles.forEach((p) => renderPole(p));
updateTable();
updateConnections();
}
function updateConnections() {
connectionLayer.clearLayers();
if (poles.length < 2) return;
const sorted = [...poles].sort((a, b) => a.id - b.id);
for (let i = 0; i < sorted.length - 1; i++) {
const a = sorted[i];
const b = sorted[i + 1];
const polyline = L.polyline(
[
[a.lat, a.lng],
[b.lat, b.lng],
],
{ color: "#64748b", weight: 2, dashArray: "6,4", opacity: 0.7 }
).addTo(connectionLayer);
const midLat = (a.lat + b.lat) / 2;
const midLng = (a.lng + b.lng) / 2;
const dist = calcDistance(a.lat, a.lng, b.lat, b.lng);
L.marker([midLat, midLng], {
icon: L.divIcon({
className: "dist-label",
html: `<div style="
background:rgba(255,255,255,.9);padding:2px 8px;
border-radius:4px;font-size:12px;font-weight:600;
border:1px solid #ccc;white-space:nowrap;
">${dist.toFixed(1)} м</div>`,
iconSize: [0, 0],
iconAnchor: [0, 0],
}),
}).addTo(connectionLayer);
}
}
function updateTable() {
document.getElementById("poleCount").textContent = poles.length;
const tbody = document.querySelector("#poleTable tbody");
tbody.innerHTML = "";
if (poles.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" class="text-center text-gray-500 py-4">Нет опор. Кликните на карту, чтобы добавить.</td></tr>';
return;
}
const sorted = [...poles].sort((a, b) => a.id - b.id);
sorted.forEach((p, i) => {
const type = POLE_TYPES[p.type];
const prevDist =
i > 0
? calcDistance(sorted[i - 1].lat, sorted[i - 1].lng, p.lat, p.lng).toFixed(1)
: "—";
const tr = document.createElement("tr");
tr.className = "border-b border-gray-200 hover:bg-gray-50";
tr.innerHTML = `
<td class="px-4 py-2 font-medium">${p.id}</td>
<td class="px-4 py-2"><span class="pole-badge" style="background:${type.color}">${type.name}</span></td>
<td class="px-4 py-2 font-mono text-sm">${p.lat.toFixed(6)}</td>
<td class="px-4 py-2 font-mono text-sm">${p.lng.toFixed(6)}</td>
<td class="px-4 py-2 text-right font-mono">${prevDist}</td>
<td class="px-4 py-2 text-center">
<button onclick="removePole(${p.id})" class="text-red-500 hover:text-red-700 text-sm" title="Удалить"></button>
</td>
`;
tbody.appendChild(tr);
});
}
function calcDistance(lat1, lon1, lat2, lon2) {
const R = 6371000;
const dLat = ((lat2 - lat1) * Math.PI) / 180;
const dLon = ((lon2 - lon1) * Math.PI) / 180;
const a =
Math.sin(dLat / 2) ** 2 +
Math.cos((lat1 * Math.PI) / 180) *
Math.cos((lat2 * Math.PI) / 180) *
Math.sin(dLon / 2) ** 2;
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}
function clearAll() {
if (poles.length === 0) return;
if (!confirm("Удалить все опоры?")) return;
poleLayer.clearLayers();
connectionLayer.clearLayers();
poles = [];
poleCounter = 1;
updateTable();
}
function exportSVG() {
if (poles.length === 0) return alert("Нет опор для экспорта");
const padding = 50;
const lats = poles.map((p) => p.lat);
const lngs = poles.map((p) => p.lng);
const minLat = Math.min(...lats);
const maxLat = Math.max(...lats);
const minLng = Math.min(...lngs);
const maxLng = Math.max(...lngs);
const latRange = maxLat - minLat || 0.001;
const lngRange = maxLng - minLng || 0.001;
const scale = 500 / Math.max(latRange, lngRange);
const toX = (lng) => padding + ((lng - minLng) / lngRange) * 500;
const toY = (lat) => padding + ((maxLat - lat) / latRange) * 500;
const w = 500 + padding * 2;
const h = 500 + padding * 2;
let svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${w} ${h}" width="${w}" height="${h}">
<style>
.pole-label{font:bold 10px sans-serif;text-anchor:middle;fill:#fff}
.dist-label{font:9px sans-serif;text-anchor:middle;fill:#333}
.title{font:14px sans-serif;font-weight:700;fill:#333}
</style>
<rect width="100%" height="100%" fill="#f8f9fa"/>
<text x="${w / 2}" y="24" text-anchor="middle" class="title">Схема расположения опор</text>`;
const sorted = [...poles].sort((a, b) => a.id - b.id);
// connections
for (let i = 0; i < sorted.length - 1; i++) {
const a = sorted[i],
b = sorted[i + 1];
const dist = calcDistance(a.lat, a.lng, b.lat, b.lng).toFixed(1);
const mx = (toX(a.lng) + toX(b.lng)) / 2;
const my = (toY(a.lat) + toY(b.lat)) / 2;
svg += `<line x1="${toX(a.lng)}" y1="${toY(a.lat)}" x2="${toX(b.lng)}" y2="${toY(b.lat)}" stroke="#94a3b8" stroke-width="1.5" stroke-dasharray="5,3"/>`;
svg += `<text x="${mx}" y="${my - 6}" class="dist-label">${dist} м</text>`;
}
// poles
sorted.forEach((p) => {
const type = POLE_TYPES[p.type];
const cx = toX(p.lng);
const cy = toY(p.lat);
svg += `<circle cx="${cx}" cy="${cy}" r="12" fill="${type.color}" stroke="#fff" stroke-width="2"/>`;
svg += `<text x="${cx}" y="${cy + 3}" class="pole-label">${p.id}</text>`;
svg += `<text x="${cx + 18}" y="${cy + 3}" font-size="8" fill="#666" font-family="sans-serif">${p.label} (${type.name})</text>`;
});
// Legend
const legendY = h - 40;
svg += `<rect x="${padding}" y="${legendY - 10}" width="${w - padding * 2}" height="2" fill="#e2e8f0"/>`;
let lx = padding;
Object.entries(POLE_TYPES).forEach(([k, t]) => {
svg += `<circle cx="${lx + 6}" cy="${legendY + 10}" r="6" fill="${t.color}" stroke="#ccc" stroke-width="1"/>`;
svg += `<text x="${lx + 16}" y="${legendY + 14}" font-size="9" fill="#555" font-family="sans-serif">${t.name}</text>`;
lx += 140;
});
svg += `</svg>`;
downloadFile(svg, "opory.svg", "image/svg+xml");
}
function exportVSD() {
alert(
"VSD (Visio) — бинарный формат. SVG можно открыть в Visio.\n\n" +
"Экспортируйте в SVG, затем откройте в Visio: Файл → Открыть → выберите .svg\n" +
"Или перетащите SVG-файл в окно Visio."
);
}
function downloadFile(content, filename, mime) {
const blob = new Blob([content], { type: mime });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
document.addEventListener("DOMContentLoaded", initApp);

294
js/kz-data.js Normal file
View File

@ -0,0 +1,294 @@
const KZ_DATA = [
{
oblast: "Акмолинская", center: [52.25, 69.35],
rayons: [
{ rayon: "Целиноградский", villages: ["Косшы", "Кабанбай батыра", "Тайтобе", "Акмол"] },
{ rayon: "Аккольский", villages: ["Акколь", "Енбек", "Жалгызкарагай"] },
{ rayon: "Аршалынский", villages: ["Аршалы", "Ижевское", "Булаксай"] },
{ rayon: "Астраханский", villages: ["Астраханка", "Жалтыр", "Новочеркасское"] },
{ rayon: "Атбасарский", villages: ["Атбасар", "Сочинское", "Борисовка"] },
{ rayon: "Буландынский", villages: ["Макинск", "Пушкино", "Айнаколь"] },
{ rayon: "Бурабайский", villages: ["Щучинск", "Бурабай", "Зеленый Бор"] },
{ rayon: "Егиндыкольский", villages: ["Егиндыколь", "Абай", "Коркыт"] },
{ rayon: "Ерейментауский", villages: ["Ерейментау", "Ынтымак", "Торгай"] },
{ rayon: "Есильский", villages: ["Есиль", "Каратал", "Бузулук"] },
{ rayon: "Жаксынский", villages: ["Жаксы", "Беловодское", "Кайракты"] },
{ rayon: "Жаркаинский", villages: ["Державинск", "Пригородное", "Тасоткель"] },
{ rayon: "Зерендинский", villages: ["Зеренда", "Викторовка", "Кызылагаш"] },
{ rayon: "Коргалжынский", villages: ["Коргалжын", "Талдыколь", "Майшукыр"] },
{ rayon: "Сандыктауский", villages: ["Балкашино", "Максимовка", "Сандыктау"] },
{ rayon: "Биржан сал", villages: ["Степняк", "Кенесары", "Андыкожа батыра"] },
]
},
{
oblast: "Актюбинская", center: [50.28, 57.21],
rayons: [
{ rayon: "Актобе", villages: ["Актобе", "Киргизское", "Курайли"] },
{ rayon: "Алгинский", villages: ["Алга", "Бескоспа", "Тамды"] },
{ rayon: "Айтекебийский", villages: ["Темирбека Жургенова", "Аккудык", "Торткудык"] },
{ rayon: "Байганинский", villages: ["Карауылкельды", "Кемерши", "Кызылбулак"] },
{ rayon: "Иргизский", villages: ["Иргиз", "Аманколь", "Каракудык"] },
{ rayon: "Каргалинский", villages: ["Богетсай", "Алимбет", "Косестек"] },
{ rayon: "Мартукский", villages: ["Мартук", "Каратаусай", "Кызылжар"] },
{ rayon: "Мугалжарский", villages: ["Кандыагаш", "Енбек", "Жагабулак"] },
{ rayon: "Темирский", villages: ["Шубаркудык", "Шубарши", "Кенкияк"] },
{ rayon: "Уилский", villages: ["Уил", "Карасу", "Сандыколь"] },
{ rayon: "Хобдинский", villages: ["Кобда", "Кызылжар", "Байтарак"] },
{ rayon: "Хромтауский", villages: ["Хромтау", "Абай", "Копта"] },
{ rayon: "Шалкарский", villages: ["Шалкар", "Биршогыр", "Тогыз"] },
]
},
{
oblast: "Алматинская", center: [44.85, 76.95],
rayons: [
{ rayon: "Конаев", villages: ["Конаев", "Шенгельды", "Коккайнар"] },
{ rayon: "Балхашский", villages: ["Баканас", "Акжиде", "Караой"] },
{ rayon: "Енбекшиказахский", villages: ["Есик", "Тургень", "Маловодное"] },
{ rayon: "Жамбылский", villages: ["Узынагаш", "Каргалы", "Жамбыл"] },
{ rayon: "Илийский", villages: ["Отеген батыра", "Боралдай", "Ащыбулак"] },
{ rayon: "Карасайский", villages: ["Каскелен", "Шамалган", "Айтей"] },
{ rayon: "Кегенский", villages: ["Кеген", "Талды", "Жанатай"] },
{ rayon: "Райымбекский", villages: ["Нарынкол", "Карасаз", "Жанасу"] },
{ rayon: "Талгарский", villages: ["Талгар", "Белбулак", "Достык"] },
{ rayon: "Уйгурский", villages: ["Чунджа", "Кетмень", "Ават"] },
]
},
{
oblast: "Атырауская", center: [47.1, 51.88],
rayons: [
{ rayon: "Атырау", villages: ["Атырау", "Томарлы", "Аксай"] },
{ rayon: "Индерский", villages: ["Индербор", "Коктогай", "Орлик"] },
{ rayon: "Исатайский", villages: ["Исатай", "Зинеден", "Тущыкудык"] },
{ rayon: "Кзылкогинский", villages: ["Кызылкога", "Жамбыл", "Коныстану"] },
{ rayon: "Курмангазинский", villages: ["Курмангазы", "Азгир", "Балкудук"] },
{ rayon: "Макатский", villages: ["Макат", "Байчунас", "Доссор"] },
{ rayon: "Махамбетский", villages: ["Махамбет", "Бейбарыс", "Акжайык"] },
]
},
{
oblast: "Западно-Казахстанская", center: [51.23, 51.37],
rayons: [
{ rayon: "Уральск", villages: ["Уральск", "Круглоозерное", "Деркул"] },
{ rayon: "Акжаикский", villages: ["Тайпак", "Алгабас", "Базартобе"] },
{ rayon: "Бокейординский", villages: ["Сайхин", "Бисен", "Каратобе"] },
{ rayon: "Бурлинский", villages: ["Аксай", "Пугачево", "Кентубек"] },
{ rayon: "Жангалинский", villages: ["Жангала", "Кызылоба", "Копжасар"] },
{ rayon: "Жанибекский", villages: ["Жанибек", "Камысты", "Куйгенколь"] },
{ rayon: "Казталовский", villages: ["Казталовка", "Караоба", "Болашак"] },
{ rayon: "Каратобинский", villages: ["Каратобе", "Жусандой", "Акжол"] },
{ rayon: "Сырымский", villages: ["Жымпиты", "Аралтобе", "Кособа"] },
{ rayon: "Таскалинский", villages: ["Таскала", "Достык", "Каменка"] },
{ rayon: "Теректинский", villages: ["Федоровка", "Тонкерис", "Акжайык"] },
{ rayon: "Чингирлауский", villages: ["Чингирлау", "Шынгырлау", "Аксу"] },
]
},
{
oblast: "Жамбылская", center: [42.82, 71.43],
rayons: [
{ rayon: "Тараз", villages: ["Тараз", "Карасу", "Кулан"] },
{ rayon: "Байзакский", villages: ["Сарыкемер", "Коптерек", "Бурыл"] },
{ rayon: "Жамбылский", villages: ["Аса", "Каратобе", "Айша-Биби"] },
{ rayon: "Жуалынский", villages: ["Курдай", "Актобе", "Кокбастау"] },
{ rayon: "Кордайский", villages: ["Кордай", "Масанчи", "Карасу"] },
{ rayon: "Меркенский", villages: ["Мерке", "Тау", "Акарал"] },
{ rayon: "Мойынкумский", villages: ["Мойынкум", "Бидайколь", "Байтал"] },
{ rayon: "Сарысуский", villages: ["Жанатас", "Сарысу", "Байкадам"] },
{ rayon: "Таласский", villages: ["Акколь", "Кенен", "Сегизсай"] },
{ rayon: "Турара Рыскулова", villages: ["Кулан", "Каменка", "Абай"] },
{ rayon: "Шуский", villages: ["Толе би", "Шу", "Акшу"] },
]
},
{
oblast: "Жетысуская", center: [45.0, 78.38],
rayons: [
{ rayon: "Талдыкорган", villages: ["Талдыкорган", "Еркин", "Каратал"] },
{ rayon: "Аксуский", villages: ["Жансугуров", "Аксу", "Мукыр"] },
{ rayon: "Алакольский", villages: ["Кабанбай", "Достык", "Кокжар"] },
{ rayon: "Ескельдинский", villages: ["Карабулак", "Талдыбулак", "Кайнарлы"] },
{ rayon: "Каратальский", villages: ["Уштобе", "Каратал", "Айдарлы"] },
{ rayon: "Кербулакский", villages: ["Сарыозек", "Сарыбулак", "Кулык"] },
{ rayon: "Коксуский", villages: ["Балпык би", "Коксу", "Майлыбас"] },
{ rayon: "Панфиловский", villages: ["Жаркент", "Аухатты", "Коктал"] },
{ rayon: "Саркандский", villages: ["Сарканд", "Амангельды", "Караой"] },
]
},
{
oblast: "Карагандинская", center: [49.8, 73.09],
rayons: [
{ rayon: "Караганда", villages: ["Караганда", "Сортировка", "Кокпекты"] },
{ rayon: "Абайский", villages: ["Абай", "Топар", "Карабас"] },
{ rayon: "Актогайский", villages: ["Актогай", "Нуркен", "Сарышаган"] },
{ rayon: "Бухар-Жырауский", villages: ["Ботакара", "Габидена Мустафина", "Самарканд"] },
{ rayon: "Каркаралинский", villages: ["Каркаралинск", "Егиндыбулак", "Коянды"] },
{ rayon: "Нуринский", villages: ["Баршино", "Нура", "Кызылжар"] },
{ rayon: "Осакаровский", villages: ["Осакаровка", "Молодежный", "Карагайлы"] },
{ rayon: "Шетский", villages: ["Коктенколь", "Аксу-Аюлы", "Мойынты"] },
]
},
{
oblast: "Костанайская", center: [53.22, 63.62],
rayons: [
{ rayon: "Костанай", villages: ["Костанай", "Затобольск", "Ульяновское"] },
{ rayon: "Алтынсаринский", villages: ["Убаган", "Докучаевка", "Кутиховка"] },
{ rayon: "Амангельдинский", villages: ["Амангельды", "Карасу", "Бидайык"] },
{ rayon: "Аулиекольский", villages: ["Аулиеколь", "Диевка", "Сулыколь"] },
{ rayon: "Беимбета Майлина", villages: ["Науырзым", "Тарановское", "Баталы"] },
{ rayon: "Денисовский", villages: ["Денисовка", "Архангельское", "Глебовка"] },
{ rayon: "Жангельдинский", villages: ["Торгай", "Калмакколь", "Айшырык"] },
{ rayon: "Житикаринский", villages: ["Житикара", "Кировка", "Степной"] },
{ rayon: "Камыстинский", villages: ["Камысты", "Карабатыр", "Аралколь"] },
{ rayon: "Карабалыкский", villages: ["Карабалык", "Победа", "Славянка"] },
{ rayon: "Карасуский", villages: ["Карасу", "Октябрьское", "Койбагар"] },
{ rayon: "Костанайский", villages: ["Тобыл", "Москалевка", "Александровка"] },
{ rayon: "Мендыкаринский", villages: ["Боровской", "Каменка", "Первомайское"] },
{ rayon: "Сарыкольский", villages: ["Сарыколь", "Большие Дубравы", "Лесной"] },
{ rayon: "Узункольский", villages: ["Узунколь", "Ряжское", "Пресногорьковка"] },
{ rayon: "Федоровский", villages: ["Федоровка", "Веселоярка", "Пешковка"] },
]
},
{
oblast: "Кызылординская", center: [44.85, 65.52],
rayons: [
{ rayon: "Кызылорда", villages: ["Кызылорда", "Кызылжарма", "Талсуат"] },
{ rayon: "Аральский", villages: ["Аральск", "Жанаконыс", "Аманоткель"] },
{ rayon: "Жалагашский", villages: ["Жалагаш", "Торткудык", "Енбек"] },
{ rayon: "Казалинский", villages: ["Айтеке би", "Уркендеу", "Кызылкум"] },
{ rayon: "Кармакшинский", villages: ["Жосалы", "Далабай", "Торетам"] },
{ rayon: "Сырдарьинский", villages: ["Теренозек", "Когалыколь", "Кызылкак"] },
{ rayon: "Чиилийский", villages: ["Шиели", "Тартогай", "Балаби"] },
{ rayon: "Яныкурганский", villages: ["Жанибек", "Когалы", "Куйган"] },
]
},
{
oblast: "Мангистауская", center: [43.65, 51.15],
rayons: [
{ rayon: "Актау", villages: ["Актау", "Умирзак", "Сауга"] },
{ rayon: "Бейнеуский", villages: ["Бейнеу", "Турыш", "Акжигит"] },
{ rayon: "Каракиянский", villages: ["Курык", "Баутино", "Каракия"] },
{ rayon: "Мангистауский", villages: ["Жетыбай", "Шетпе", "Аксу"] },
{ rayon: "Мунайлинский", villages: ["Мангистау", "Тенге", "Баянды"] },
{ rayon: "Тупкараганский", villages: ["Форт-Шевченко", "Таучик", "Баутино"] },
]
},
{
oblast: "Павлодарская", center: [52.28, 76.95],
rayons: [
{ rayon: "Павлодар", villages: ["Павлодар", "Кенжеколь", "Мойылды"] },
{ rayon: "Аккулинский", villages: ["Акку", "Амангельды", "Кызылжар"] },
{ rayon: "Актогайский", villages: ["Актогай", "Харьковка", "Куттерген"] },
{ rayon: "Баянаульский", villages: ["Баянаул", "Каракамыс", "Бирлик"] },
{ rayon: "Железинский", villages: ["Железинка", "Енбекши", "Башмачное"] },
{ rayon: "Иртышский", villages: ["Иртышск", "Голубовка", "Панфилово"] },
{ rayon: "Майский", villages: ["Коктобе", "Майкарагай", "Кентубек"] },
{ rayon: "Павлодарский", villages: ["Ленинский", "Черноярка", "Красноармейка"] },
{ rayon: "Теренкольский", villages: ["Теренколь", "Береговое", "Тогузак"] },
{ rayon: "Успенский", villages: ["Успенка", "Богодаровка", "Чернорецк"] },
{ rayon: "Щербактинский", villages: ["Щербакты", "Галкино", "Алексеевка"] },
]
},
{
oblast: "Северо-Казахстанская", center: [54.87, 69.14],
rayons: [
{ rayon: "Петропавловск", villages: ["Петропавловск", "Новопавловка", "Бишкуль"] },
{ rayon: "Айыртауский", villages: ["Саумалколь", "Имантау", "Лобаново"] },
{ rayon: "Акжарский", villages: ["Талшик", "Ленинградское", "Ульгули"] },
{ rayon: "Аккайынский", villages: ["Смирново", "Киялы", "Трудовое"] },
{ rayon: "Есильский", villages: ["Явленка", "Покровка", "Чкалово"] },
{ rayon: "Жамбылский", villages: ["Пресновка", "Май-Балык", "Вознесенка"] },
{ rayon: "Кызылжарский", villages: ["Бесколь", "Коньково", "Прибрежное"] },
{ rayon: "Магжана Жумабаева", villages: ["Булаево", "Молодогвардейское", "Возвышенка"] },
{ rayon: "Мамлютский", villages: ["Мамлютка", "Дубровное", "Покровка"] },
{ rayon: "Тайыншинский", villages: ["Тайынша", "Чкалово", "Кирово"] },
{ rayon: "Тимирязевский", villages: ["Тимирязево", "Есиль", "Комсомольское"] },
{ rayon: "Уалихановский", villages: ["Кишкенеколь", "Акбулак", "Карасу"] },
{ rayon: "Шал акына", villages: ["Сергеевка", "Новопокровка", "Пригородное"] },
]
},
{
oblast: "Туркестанская", center: [42.33, 69.59],
rayons: [
{ rayon: "Туркестан", villages: ["Туркестан", "Акжар", "Коксу"] },
{ rayon: "Темирлановка", villages: ["Темирлановка", "Каскелен", "Керейт"] },
{ rayon: "Байдибекский", villages: ["Шаян", "Боралдай", "Коктерек"] },
{ rayon: "Жетысайский", villages: ["Жетысай", "Асыката", "Ынтымак"] },
{ rayon: "Казыгуртский", villages: ["Казыгурт", "Турбат", "Шарбулак"] },
{ rayon: "Келесский", villages: ["Абай", "Кошкарата", "Каратобе"] },
{ rayon: "Мактааральский", villages: ["Атакент", "Ильичевка", "Макталы"] },
{ rayon: "Ордабасынский", villages: ["Темирлановка", "Бугунь", "Караспан"] },
{ rayon: "Отырарский", villages: ["Шаульдер", "Караконыр", "Коктобе"] },
{ rayon: "Сайрамский", villages: ["Аксукент", "Сайрам", "Манкент"] },
{ rayon: "Сарыагашский", villages: ["Сарыагаш", "Жылга", "Дарбаза"] },
{ rayon: "Созакский", villages: ["Шолаккорган", "Созак", "Каракур"] },
{ rayon: "Толебийский", villages: ["Толе би", "Каратау", "Акжар"] },
{ rayon: "Тюлькубасский", villages: ["Тюлькубас", "Майлыкент", "Алатау"] },
{ rayon: "Шардаринский", villages: ["Шардара", "Коксу", "Акшенгельды"] },
]
},
{
oblast: "Улытауская", center: [47.78, 67.7],
rayons: [
{ rayon: "Жезказган", villages: ["Жезказган", "Кенгир", "Сарыкенгир"] },
{ rayon: "Жанааркинский", villages: ["Жанаарка", "Актубек", "Байдалы"] },
{ rayon: "Улытауский", villages: ["Улытау", "Аксу-Аюлы", "Жезды"] },
{ rayon: "Сатпаев", villages: ["Сатпаев", "Карсакпай", "Кенгирское"] },
]
},
{
oblast: "Восточно-Казахстанская", center: [49.98, 82.62],
rayons: [
{ rayon: "Усть-Каменогорск", villages: ["Усть-Каменогорск", "Ахмирово", "Меновное"] },
{ rayon: "Алтайский", villages: ["Перевальное", "Снегирево", "Куйбышево"] },
{ rayon: "Глубоковский", villages: ["Глубокое", "Быструха", "Малоубинка"] },
{ rayon: "Зайсанский", villages: ["Зайсан", "Карабулак", "Акарал"] },
{ rayon: "Катон-Карагайский", villages: ["Катон-Карагай", "Улкен Нарын", "Коробиха"] },
{ rayon: "Куршимский", villages: ["Куршим", "Кызылжар", "Маралды"] },
{ rayon: "Самарский", villages: ["Самарское", "Каратюбе", "Кокжыра"] },
{ rayon: "Тарбагатайский", villages: ["Акжар", "Кокжыра", "Толеугали"] },
{ rayon: "Уланский", villages: ["Касыма Кайсенова", "Мамай батыра", "Кабанбай"] },
{ rayon: "Шемонаихинский", villages: ["Шемонаиха", "Усть-Таловка", "Первомайский"] },
]
},
{
oblast: "Абайская", center: [50.39, 80.25],
rayons: [
{ rayon: "Семей", villages: ["Семей", "Карауыл", "Знаменка"] },
{ rayon: "Абайский", villages: ["Карауыл", "Кокбай", "Кунанбай"] },
{ rayon: "Аягозский", villages: ["Аягоз", "Тарлаулы", "Мадениет"] },
{ rayon: "Бескарагайский", villages: ["Бескарагай", "Долонь", "Канонерка"] },
{ rayon: "Бородулихинский", villages: ["Бородулиха", "Новопокровка", "Новодворовка"] },
{ rayon: "Жанасемейский", villages: ["Кайнар", "Коркыт", "Каменка"] },
{ rayon: "Кокпектинский", villages: ["Кокпекты", "Тасшокы", "Кокжайык"] },
{ rayon: "Урджарский", villages: ["Урджар", "Каракол", "Маканчи"] },
]
},
{
oblast: "Астана", center: [51.13, 71.43],
rayons: [
{ rayon: "Алматинский", villages: ["Астана", "Аксу", "Интернациональный"] },
{ rayon: "Байконурский", villages: ["Астана", "Куйгенжар", "Каражар"] },
{ rayon: "Есильский", villages: ["Астана", "Зеленый Квартал", "Жагалау"] },
{ rayon: "Сарыаркинский", villages: ["Астана", "Коктал", "Пригородное"] },
]
},
{
oblast: "Алматы", center: [43.24, 76.89],
rayons: [
{ rayon: "Алмалы", villages: ["Алматы", "Каменка", "Гульдала"] },
{ rayon: "Ауэзов", villages: ["Алматы", "Калкаман", "Теректы"] },
{ rayon: "Бостандык", villages: ["Алматы", "Нур-Алатау", "Карагайлы"] },
{ rayon: "Жетысу", villages: ["Алматы", "Айнабулак", "Коктобе"] },
{ rayon: "Медеу", villages: ["Алматы", "Кумбель", "Бутаковка"] },
{ rayon: "Наурызбай", villages: ["Алматы", "Таугуль", "Калкаман"] },
{ rayon: "Турксиб", villages: ["Алматы", "Жулдыз", "Отеген"] },
]
},
{
oblast: "Шымкент", center: [42.32, 69.60],
rayons: [
{ rayon: "Абай", villages: ["Шымкент", "Сайрам", "Каратобе"] },
{ rayon: "Енбекши", villages: ["Шымкент", "Туран", "Жанаталап"] },
{ rayon: "Каратау", villages: ["Шымкент", "Кызылжар", "Тайбурыл"] },
{ rayon: "Аль-Фараби", villages: ["Шымкент", "Акжайык", "Казына"] },
]
}
];