let map, poleLayer, connectionLayer, tileLayer; let poles = []; let poleCounter = 1; let currentPoleType = "1"; let currentPoleStatus = "projected"; let currentExistingType = "communication"; let history = []; let historyIndex = -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: "★" }, }; const TILE_PROVIDERS = { street: { name: "Схема", url: "https://a.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png", attr: "© CARTO" }, satellite: { name: "Спутник", url: "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}", attr: "© Esri" }, }; function initApp() { initMap(); initSelectors(); initPoleControls(); updateTable(); updateUndoButtons(); } function initMap() { map = L.map("map", { zoomControl: true }).setView([48.0, 68.0], 6); tileLayer = L.tileLayer(TILE_PROVIDERS.street.url, { attribution: TILE_PROVIDERS.street.attr, maxZoom: 19 }).addTo(map); poleLayer = L.featureGroup().addTo(map); connectionLayer = L.featureGroup().addTo(map); map.on("click", function (e) { saveHistory(); addPole(e.latlng.lat, e.latlng.lng); }); document.querySelectorAll(".map-mode-btn").forEach((btn) => { btn.addEventListener("click", function () { document.querySelectorAll(".map-mode-btn").forEach((b) => b.classList.remove("active")); this.classList.add("active"); const mode = this.dataset.mode; const provider = TILE_PROVIDERS[mode]; tileLayer.setUrl(provider.url); tileLayer.setAttribution(provider.attr); }); }); } function saveHistory() { const snapshot = poles.map((p) => ({ ...p })); if (historyIndex < history.length - 1) { history = history.slice(0, historyIndex + 1); } history.push(snapshot); historyIndex = history.length - 1; } function undo() { if (historyIndex < 0) return; historyIndex--; restoreHistory(); } function redo() { if (historyIndex >= history.length - 1) return; historyIndex++; restoreHistory(); } function restoreHistory() { if (historyIndex < 0 || historyIndex >= history.length) { poles = []; } else { poles = history[historyIndex].map((p) => ({ ...p })); } poleCounter = poles.length > 0 ? Math.max(...poles.map((p) => p.id)) + 1 : 1; poleLayer.clearLayers(); connectionLayer.clearLayers(); poles.forEach((p) => renderPole(p)); updateTable(); updateConnections(); updateUndoButtons(); } function updateUndoButtons() { document.getElementById("undoBtn").style.opacity = historyIndex >= 0 ? "1" : "0.3"; document.getElementById("redoBtn").style.opacity = historyIndex < history.length - 1 ? "1" : "0.3"; } function initSelectors() { const oblastSel = document.getElementById("oblast"); const rayonSel = document.getElementById("rayon"); const villageSel = document.getElementById("village"); const searchInput = document.getElementById("searchInput"); const manualVillage = document.getElementById("manualVillage"); KZ_DATA.forEach((o) => { const opt = document.createElement("option"); opt.value = o.oblast; opt.textContent = o.oblast; oblastSel.appendChild(opt); }); function updateRayons() { rayonSel.innerHTML = ''; villageSel.innerHTML = ''; 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} (${r.villages.length} сёл)`; rayonSel.appendChild(opt); }); } function updateVillages() { villageSel.innerHTML = ''; 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", function() { updateRayons(); const oblast = KZ_DATA.find((o) => o.oblast === this.value); if (oblast) map.setView(oblast.center, 8); }); rayonSel.addEventListener("change", function() { updateVillages(); const oblast = KZ_DATA.find((o) => o.oblast === oblastSel.value); if (!oblast) return; const rayon = oblast.rayons.find((r) => r.rayon === this.value); const center = (rayon && rayon.center) ? rayon.center : (oblast ? oblast.center : null); if (center) map.setView(center, 10); }); function goToVillage(name) { if (!name) return; const oblast = KZ_DATA.find((o) => o.oblast === oblastSel.value); if (!oblast) return; const rayon = oblast.rayons.find((r) => r.rayon === rayonSel.value); const center = (rayon && rayon.center) ? rayon.center : oblast.center; map.setView(center, 12); L.popup() .setLatLng(center) .setContent(`${name}
${oblast.oblast}, ${rayon ? rayon.rayon : ''}`) .openOn(map); } villageSel.addEventListener("change", function () { goToVillage(this.value); }); manualVillage.addEventListener("keydown", function (e) { if (e.key === "Enter" && this.value.trim()) { goToVillage(this.value.trim()); } }); manualVillage.addEventListener("blur", function () { if (this.value.trim()) { goToVillage(this.value.trim()); } }); searchInput.addEventListener("input", function () { const q = this.value.toLowerCase().trim(); const list = document.getElementById("searchResults"); list.innerHTML = ""; 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: r.center || o.center }); } }); }); }); if (results.length === 0) { list.innerHTML = "
  • Не найдено
  • "; return; } results.slice(0, 30).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(`${r.village}
    ${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"); document.querySelectorAll(".pole-status-btn").forEach((btn) => { btn.addEventListener("click", function () { document.querySelectorAll(".pole-status-btn").forEach((b) => b.classList.remove("active")); this.classList.add("active"); currentPoleStatus = this.dataset.status; const existingTypeSel = document.getElementById("existingTypeGroup"); existingTypeSel.style.display = currentPoleStatus === "existing" ? "block" : "none"; }); }); document.querySelectorAll(".existing-type-btn").forEach((btn) => { btn.addEventListener("click", function () { document.querySelectorAll(".existing-type-btn").forEach((b) => b.classList.remove("active")); this.classList.add("active"); currentExistingType = this.dataset.existing; }); }); document.querySelector('[data-status="projected"]').classList.add("active"); } const EXISTING_TYPES = { communication: "опора связи", power: "ЛЭП", lighting: "освещения", }; function addPole(lat, lng) { const type = POLE_TYPES[currentPoleType]; const id = poleCounter++; const pole = { id, lat, lng, type: currentPoleType, status: currentPoleStatus, existingType: currentExistingType, label: `Опора ${id}` }; poles.push(pole); renderPole(pole); updateTable(); updateConnections(); updateUndoButtons(); } function renderPole(pole) { const type = POLE_TYPES[pole.type]; const icon = L.divIcon({ className: "pole-marker", html: `
    ${pole.id}
    `, iconSize: [28, 28], iconAnchor: [14, 14], }); const marker = L.marker([pole.lat, pole.lng], { icon }).addTo(poleLayer); const statusLabel = pole.status === "existing" ? `Существующая (${EXISTING_TYPES[pole.existingType] || pole.existingType})` : "Проектируемая"; marker.bindPopup(` ${pole.label}
    Тип: ${type.name}
    Статус: ${statusLabel}
    Координаты: ${pole.lat.toFixed(6)}, ${pole.lng.toFixed(6)}
    `); pole._marker = marker; } function removePoleById(id) { saveHistory(); const idx = poles.findIndex((p) => p.id === id); if (idx === -1) return; if (poles[idx]._marker) poleLayer.removeLayer(poles[idx]._marker); poles.splice(idx, 1); renumberPoles(); } function removePoleFromTable(id) { saveHistory(); removePoleById(id); } function renumberPoles() { 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(); updateUndoButtons(); } 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]; 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: `
    ${dist.toFixed(1)} м
    `, 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 = 'Нет опор. Кликните на карту, чтобы добавить.'; 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 statusLabel = p.status === "existing" ? `Сущ. (${EXISTING_TYPES[p.existingType] || p.existingType})` : "Проект."; const statusColor = p.status === "existing" ? "#f59e0b" : "#10b981"; const tr = document.createElement("tr"); tr.className = "border-b border-gray-200 hover:bg-gray-50"; tr.innerHTML = ` ${p.id} ${type.name} ${statusLabel} ${p.lat.toFixed(6)} ${p.lng.toFixed(6)} ${prevDist} `; 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; saveHistory(); poleLayer.clearLayers(); connectionLayer.clearLayers(); poles = []; poleCounter = 1; updateTable(); updateConnections(); updateUndoButtons(); } 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 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; const sorted = [...poles].sort((a, b) => a.id - b.id); let svg = ` Схема расположения опор`; 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 += ``; svg += `${dist} м`; } sorted.forEach((p) => { const type = POLE_TYPES[p.type]; const cx = toX(p.lng), cy = toY(p.lat); svg += ``; svg += `${p.id}`; const stLabel = p.status === "existing" ? `сущ.${EXISTING_TYPES[p.existingType]}` : "проект."; svg += `${p.label} (${type.name}, ${stLabel})`; }); const legY = h - 40; svg += ``; let lx = padding; Object.entries(POLE_TYPES).forEach(([, t]) => { svg += ``; svg += `${t.name}`; lx += 140; }); svg += ``; downloadFile(svg, "opory.svg", "image/svg+xml"); } function exportVSD() { alert('VSD (Visio) — бинарный формат.\n\nЭкспортируйте в SVG, затем откройте 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);