pole-map/js/app.js
2026-06-24 07:50:00 +00:00

380 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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