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: "© 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 = ''; 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; rayonSel.appendChild(opt); }); updateVillages(); } 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", 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(`${name}
${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 = "
  • Слишком много результатов, уточните
  • "; 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(`${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"); } 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: `
    ${pole.id}
    `, iconSize: [28, 28], iconAnchor: [14, 14], }); const marker = L.marker([pole.lat, pole.lng], { icon }).addTo(poleLayer); marker.bindPopup(` ${pole.label}
    Тип: ${type.name}
    Координаты: ${pole.lat.toFixed(6)}, ${pole.lng.toFixed(6)}
    `); 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: `
    ${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 tr = document.createElement("tr"); tr.className = "border-b border-gray-200 hover:bg-gray-50"; tr.innerHTML = ` ${p.id} ${type.name} ${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; 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 = ` Схема расположения опор`; 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 += ``; svg += `${dist} м`; } // poles sorted.forEach((p) => { const type = POLE_TYPES[p.type]; const cx = toX(p.lng); const cy = toY(p.lat); svg += ``; svg += `${p.id}`; svg += `${p.label} (${type.name})`; }); // Legend const legendY = h - 40; svg += ``; let lx = padding; Object.entries(POLE_TYPES).forEach(([k, t]) => { svg += ``; svg += `${t.name}`; lx += 140; }); 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);