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 = ``;
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);