v2: статус опор + проектируемая/существующая
This commit is contained in:
parent
8b031562ba
commit
22d6e85b94
68
index.html
68
index.html
@ -22,6 +22,11 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Inter,system-ui,san
|
|||||||
.btn-primary:hover{background:#1e293b}
|
.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{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}
|
.btn-secondary:hover{border-color:#94a3b8}
|
||||||
|
.btn-icon{background:transparent;color:#0f172a;padding:9px 12px;border-radius:8px;font-weight:500;border:2px solid #cbd5e1;cursor:pointer;transition:all .15s;font-size:16px;line-height:1}
|
||||||
|
.btn-icon:hover{border-color:#94a3b8;background:#f8fafc}
|
||||||
|
.map-mode-btn{padding:6px 14px;border-radius:8px;border:2px solid #e2e8f0;cursor:pointer;font-size:12px;font-weight:500;transition:all .15s;background:#fff}
|
||||||
|
.map-mode-btn:hover{border-color:#94a3b8}
|
||||||
|
.map-mode-btn.active{border-color:#0f172a;background:#0f172a;color:#fff}
|
||||||
input,select{width:100%;padding:10px 14px;border:1px solid #e2e8f0;border-radius:8px;font-size:14px;background:#fff;outline:none}
|
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}
|
input:focus,select:focus{border-color:#0f172a}
|
||||||
table{width:100%;border-collapse:collapse;font-size:14px}
|
table{width:100%;border-collapse:collapse;font-size:14px}
|
||||||
@ -38,12 +43,27 @@ table td{padding:10px 16px;border-bottom:1px solid #f1f5f9}
|
|||||||
<div class="flex h-screen overflow-hidden">
|
<div class="flex h-screen overflow-hidden">
|
||||||
<!-- Sidebar -->
|
<!-- Sidebar -->
|
||||||
<div class="w-[420px] min-w-[420px] bg-white border-r border-gray-200 flex flex-col overflow-hidden">
|
<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">
|
<div class="px-5 py-4 border-b border-gray-200 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
<h1 class="text-xl font-bold">PoleMap</h1>
|
<h1 class="text-xl font-bold">PoleMap</h1>
|
||||||
<p class="text-sm text-gray-500 mt-1">Расстановка опор связи на карте</p>
|
<p class="text-sm text-gray-500 mt-1">Расстановка опор связи</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-1">
|
||||||
|
<button id="undoBtn" class="btn-icon" onclick="undo()" title="Откатить">↩</button>
|
||||||
|
<button id="redoBtn" class="btn-icon" onclick="redo()" title="Вернуть">↪</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1 overflow-y-auto scrollbar-thin px-5 py-4 space-y-4">
|
||||||
|
<!-- Map mode -->
|
||||||
|
<div>
|
||||||
|
<label class="text-xs font-semibold uppercase tracking-wider text-gray-500 mb-1.5 block">Вид карты</label>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button class="map-mode-btn active" data-mode="street">🗺 Схема</button>
|
||||||
|
<button class="map-mode-btn" data-mode="satellite">🛰 Спутник</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-1 overflow-y-auto scrollbar-thin px-5 py-4 space-y-5">
|
|
||||||
<!-- Search -->
|
<!-- Search -->
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<label class="text-xs font-semibold uppercase tracking-wider text-gray-500 mb-1.5 block">Поиск села</label>
|
<label class="text-xs font-semibold uppercase tracking-wider text-gray-500 mb-1.5 block">Поиск села</label>
|
||||||
@ -52,36 +72,59 @@ table td{padding:10px 16px;border-bottom:1px solid #f1f5f9}
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Selectors -->
|
<!-- Selectors -->
|
||||||
<div class="space-y-3">
|
<div class="space-y-2">
|
||||||
<div>
|
<div>
|
||||||
<label class="text-xs font-semibold uppercase tracking-wider text-gray-500 mb-1.5 block">Область</label>
|
<label class="text-xs font-semibold uppercase tracking-wider text-gray-500 mb-1 block">Область</label>
|
||||||
<select id="oblast"><option value="">— Выберите область —</option></select>
|
<select id="oblast"><option value="">— Выберите область —</option></select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="text-xs font-semibold uppercase tracking-wider text-gray-500 mb-1.5 block">Район</label>
|
<label class="text-xs font-semibold uppercase tracking-wider text-gray-500 mb-1 block">Район</label>
|
||||||
<select id="rayon"><option value="">— Выберите район —</option></select>
|
<select id="rayon"><option value="">— Выберите район —</option></select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="text-xs font-semibold uppercase tracking-wider text-gray-500 mb-1.5 block">Село</label>
|
<label class="text-xs font-semibold uppercase tracking-wider text-gray-500 mb-1 block">Село</label>
|
||||||
<select id="village"><option value="">— Выберите село —</option></select>
|
<select id="village"><option value="">— Выберите село —</option></select>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="text-xs font-semibold uppercase tracking-wider text-gray-500 mb-1 block">Или введите название</label>
|
||||||
|
<input id="manualVillage" type="text" placeholder="Например: Косшы, Жетыген..." autocomplete="off">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pole status -->
|
||||||
|
<div>
|
||||||
|
<label class="text-xs font-semibold uppercase tracking-wider text-gray-500 mb-2 block">Статус опоры</label>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button class="pole-status-btn active" data-status="projected" style="padding:7px 16px;border-radius:8px;border:2px solid #e2e8f0;cursor:pointer;font-size:13px;font-weight:500;background:#fff;flex:1">🆕 Проектируемая</button>
|
||||||
|
<button class="pole-status-btn" data-status="existing" style="padding:7px 16px;border-radius:8px;border:2px solid #e2e8f0;cursor:pointer;font-size:13px;font-weight:500;background:#fff;flex:1">🏗 Существующая</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Existing type (shown when existing selected) -->
|
||||||
|
<div id="existingTypeGroup" style="display:none">
|
||||||
|
<label class="text-xs font-semibold uppercase tracking-wider text-gray-500 mb-2 block">Тип существующей опоры</label>
|
||||||
|
<div class="flex gap-1">
|
||||||
|
<button class="existing-type-btn active" data-existing="communication" style="padding:6px 10px;border-radius:8px;border:2px solid #e2e8f0;cursor:pointer;font-size:12px;font-weight:500;background:#fff;flex:1">📡 Связи</button>
|
||||||
|
<button class="existing-type-btn" data-existing="power" style="padding:6px 10px;border-radius:8px;border:2px solid #e2e8f0;cursor:pointer;font-size:12px;font-weight:500;background:#fff;flex:1">⚡ ЛЭП</button>
|
||||||
|
<button class="existing-type-btn" data-existing="lighting" style="padding:6px 10px;border-radius:8px;border:2px solid #e2e8f0;cursor:pointer;font-size:12px;font-weight:500;background:#fff;flex:1">💡 Освещения</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pole types -->
|
<!-- Pole types -->
|
||||||
<div>
|
<div>
|
||||||
<label class="text-xs font-semibold uppercase tracking-wider text-gray-500 mb-2 block">Тип опоры</label>
|
<label class="text-xs font-semibold uppercase tracking-wider text-gray-500 mb-2 block">Тип опоры (конструкция)</label>
|
||||||
<div class="grid grid-cols-2 gap-2">
|
<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="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="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="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="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>
|
<button class="pole-type-btn col-span-2" data-type="5"><span class="dot" style="background:#7c3aed"></span>С муфтой и ОРКсп</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-gray-400 mt-2">Кликните на карту, чтобы поставить опору выбранного типа</p>
|
<p class="text-xs text-gray-400 mt-2">Кликните на карту, чтобы поставить опору</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Actions -->
|
<!-- Actions -->
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2 pt-2">
|
||||||
<button class="btn-primary flex-1" onclick="exportSVG()">📥 SVG</button>
|
<button class="btn-primary flex-1" onclick="exportSVG()">📥 SVG</button>
|
||||||
<button class="btn-secondary flex-1" onclick="exportVSD()">📥 VSD</button>
|
<button class="btn-secondary flex-1" onclick="exportVSD()">📥 VSD</button>
|
||||||
<button class="btn-secondary px-3" onclick="clearAll()" title="Очистить всё">🗑</button>
|
<button class="btn-secondary px-3" onclick="clearAll()" title="Очистить всё">🗑</button>
|
||||||
@ -105,6 +148,7 @@ table td{padding:10px 16px;border-bottom:1px solid #f1f5f9}
|
|||||||
<tr>
|
<tr>
|
||||||
<th class="w-14">№</th>
|
<th class="w-14">№</th>
|
||||||
<th>Тип</th>
|
<th>Тип</th>
|
||||||
|
<th>Статус</th>
|
||||||
<th>Широта</th>
|
<th>Широта</th>
|
||||||
<th>Долгота</th>
|
<th>Долгота</th>
|
||||||
<th class="text-right">Дист. до пред.</th>
|
<th class="text-right">Дист. до пред.</th>
|
||||||
@ -112,7 +156,7 @@ table td{padding:10px 16px;border-bottom:1px solid #f1f5f9}
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr><td colspan="6" class="text-center text-gray-500 py-4">Кликните на карту, чтобы добавить опору</td></tr>
|
<tr><td colspan="7" class="text-center text-gray-500 py-4">Кликните на карту, чтобы добавить опору</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
240
js/app.js
240
js/app.js
@ -1,7 +1,11 @@
|
|||||||
let map, drawnItems, poleLayer, connectionLayer;
|
let map, poleLayer, connectionLayer, tileLayer;
|
||||||
let poles = [];
|
let poles = [];
|
||||||
let poleCounter = 1;
|
let poleCounter = 1;
|
||||||
let currentPoleType = "1";
|
let currentPoleType = "1";
|
||||||
|
let currentPoleStatus = "projected";
|
||||||
|
let currentExistingType = "communication";
|
||||||
|
let history = [];
|
||||||
|
let historyIndex = -1;
|
||||||
|
|
||||||
const POLE_TYPES = {
|
const POLE_TYPES = {
|
||||||
"1": { name: "Промежуточная", color: "#2563eb", icon: "●" },
|
"1": { name: "Промежуточная", color: "#2563eb", icon: "●" },
|
||||||
@ -11,27 +15,82 @@ const POLE_TYPES = {
|
|||||||
"5": { name: "С муфтой и ОРКсп", color: "#7c3aed", 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: "© <a href='https://carto.com'>CARTO</a>" },
|
||||||
|
satellite: { name: "Спутник", url: "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}", attr: "© Esri" },
|
||||||
|
};
|
||||||
|
|
||||||
function initApp() {
|
function initApp() {
|
||||||
initMap();
|
initMap();
|
||||||
initSelectors();
|
initSelectors();
|
||||||
initPoleControls();
|
initPoleControls();
|
||||||
updateTable();
|
updateTable();
|
||||||
|
updateUndoButtons();
|
||||||
}
|
}
|
||||||
|
|
||||||
function initMap() {
|
function initMap() {
|
||||||
map = L.map("map", { zoomControl: true }).setView([48.0, 68.0], 6);
|
map = L.map("map", { zoomControl: true }).setView([48.0, 68.0], 6);
|
||||||
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
tileLayer = L.tileLayer(TILE_PROVIDERS.street.url, { attribution: TILE_PROVIDERS.street.attr, maxZoom: 19 }).addTo(map);
|
||||||
attribution: "© OpenStreetMap",
|
|
||||||
maxZoom: 19,
|
|
||||||
}).addTo(map);
|
|
||||||
|
|
||||||
drawnItems = L.featureGroup().addTo(map);
|
|
||||||
poleLayer = L.featureGroup().addTo(map);
|
poleLayer = L.featureGroup().addTo(map);
|
||||||
connectionLayer = L.featureGroup().addTo(map);
|
connectionLayer = L.featureGroup().addTo(map);
|
||||||
|
|
||||||
map.on("click", function (e) {
|
map.on("click", function (e) {
|
||||||
|
saveHistory();
|
||||||
addPole(e.latlng.lat, e.latlng.lng);
|
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() {
|
function initSelectors() {
|
||||||
@ -39,6 +98,7 @@ function initSelectors() {
|
|||||||
const rayonSel = document.getElementById("rayon");
|
const rayonSel = document.getElementById("rayon");
|
||||||
const villageSel = document.getElementById("village");
|
const villageSel = document.getElementById("village");
|
||||||
const searchInput = document.getElementById("searchInput");
|
const searchInput = document.getElementById("searchInput");
|
||||||
|
const manualVillage = document.getElementById("manualVillage");
|
||||||
|
|
||||||
KZ_DATA.forEach((o) => {
|
KZ_DATA.forEach((o) => {
|
||||||
const opt = document.createElement("option");
|
const opt = document.createElement("option");
|
||||||
@ -55,10 +115,9 @@ function initSelectors() {
|
|||||||
oblast.rayons.forEach((r) => {
|
oblast.rayons.forEach((r) => {
|
||||||
const opt = document.createElement("option");
|
const opt = document.createElement("option");
|
||||||
opt.value = r.rayon;
|
opt.value = r.rayon;
|
||||||
opt.textContent = r.rayon;
|
opt.textContent = `${r.rayon} (${r.villages.length} сёл)`;
|
||||||
rayonSel.appendChild(opt);
|
rayonSel.appendChild(opt);
|
||||||
});
|
});
|
||||||
updateVillages();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateVillages() {
|
function updateVillages() {
|
||||||
@ -78,20 +137,36 @@ function initSelectors() {
|
|||||||
oblastSel.addEventListener("change", updateRayons);
|
oblastSel.addEventListener("change", updateRayons);
|
||||||
rayonSel.addEventListener("change", updateVillages);
|
rayonSel.addEventListener("change", updateVillages);
|
||||||
|
|
||||||
villageSel.addEventListener("change", function () {
|
function goToVillage(name) {
|
||||||
const name = this.value;
|
|
||||||
if (!name) return;
|
if (!name) return;
|
||||||
const oblast = KZ_DATA.find((o) => o.oblast === oblastSel.value);
|
const oblast = KZ_DATA.find((o) => o.oblast === oblastSel.value);
|
||||||
if (!oblast) return;
|
if (!oblast) return;
|
||||||
map.setView(oblast.center, 10);
|
map.setView(oblast.center, 12);
|
||||||
L.popup()
|
L.popup()
|
||||||
.setLatLng(oblast.center)
|
.setLatLng(oblast.center)
|
||||||
.setContent(`<b>${name}</b><br>${oblast.oblast}, ${rayonSel.value}`)
|
.setContent(`<b>${name}</b><br>${oblast.oblast}, ${rayonSel.value}`)
|
||||||
.openOn(map);
|
.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 () {
|
searchInput.addEventListener("input", function () {
|
||||||
const q = this.value.toLowerCase().trim();
|
const q = this.value.toLowerCase().trim();
|
||||||
|
const list = document.getElementById("searchResults");
|
||||||
|
list.innerHTML = "";
|
||||||
if (q.length < 2) return;
|
if (q.length < 2) return;
|
||||||
const results = [];
|
const results = [];
|
||||||
KZ_DATA.forEach((o) => {
|
KZ_DATA.forEach((o) => {
|
||||||
@ -103,13 +178,11 @@ function initSelectors() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
const list = document.getElementById("searchResults");
|
if (results.length === 0) {
|
||||||
list.innerHTML = "";
|
list.innerHTML = "<li class='px-3 py-2 text-gray-500'>Не найдено</li>";
|
||||||
if (results.length === 0 || results.length > 50) {
|
|
||||||
if (results.length > 50) list.innerHTML = "<li class='px-3 py-2 text-gray-500'>Слишком много результатов, уточните</li>";
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
results.slice(0, 20).forEach((r) => {
|
results.slice(0, 30).forEach((r) => {
|
||||||
const li = document.createElement("li");
|
const li = document.createElement("li");
|
||||||
li.className = "px-3 py-2 cursor-pointer hover:bg-cyan-50";
|
li.className = "px-3 py-2 cursor-pointer hover:bg-cyan-50";
|
||||||
li.textContent = `${r.village} — ${r.rayon}, ${r.oblast}`;
|
li.textContent = `${r.village} — ${r.rayon}, ${r.oblast}`;
|
||||||
@ -147,16 +220,42 @@ function initPoleControls() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
document.querySelector('[data-type="1"]').classList.add("active");
|
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) {
|
function addPole(lat, lng) {
|
||||||
const type = POLE_TYPES[currentPoleType];
|
const type = POLE_TYPES[currentPoleType];
|
||||||
const id = poleCounter++;
|
const id = poleCounter++;
|
||||||
const pole = { id, lat, lng, type: currentPoleType, label: `Опора ${id}` };
|
const pole = { id, lat, lng, type: currentPoleType, status: currentPoleStatus, existingType: currentExistingType, label: `Опора ${id}` };
|
||||||
poles.push(pole);
|
poles.push(pole);
|
||||||
renderPole(pole);
|
renderPole(pole);
|
||||||
updateTable();
|
updateTable();
|
||||||
updateConnections();
|
updateConnections();
|
||||||
|
updateUndoButtons();
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderPole(pole) {
|
function renderPole(pole) {
|
||||||
@ -176,21 +275,32 @@ function renderPole(pole) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const marker = L.marker([pole.lat, pole.lng], { icon }).addTo(poleLayer);
|
const marker = L.marker([pole.lat, pole.lng], { icon }).addTo(poleLayer);
|
||||||
|
const statusLabel = pole.status === "existing" ? `Существующая (${EXISTING_TYPES[pole.existingType] || pole.existingType})` : "Проектируемая";
|
||||||
marker.bindPopup(`
|
marker.bindPopup(`
|
||||||
<b>${pole.label}</b><br>
|
<b>${pole.label}</b><br>
|
||||||
Тип: ${type.name}<br>
|
Тип: ${type.name}<br>
|
||||||
|
Статус: ${statusLabel}<br>
|
||||||
Координаты: ${pole.lat.toFixed(6)}, ${pole.lng.toFixed(6)}
|
Координаты: ${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>
|
<br><button onclick="removePoleById(${pole.id})" style="color:red;cursor:pointer;border:none;background:none;margin-top:4px">✕ Удалить</button>
|
||||||
`);
|
`);
|
||||||
pole._marker = marker;
|
pole._marker = marker;
|
||||||
}
|
}
|
||||||
|
|
||||||
function removePole(id) {
|
function removePoleById(id) {
|
||||||
|
saveHistory();
|
||||||
const idx = poles.findIndex((p) => p.id === id);
|
const idx = poles.findIndex((p) => p.id === id);
|
||||||
if (idx === -1) return;
|
if (idx === -1) return;
|
||||||
const pole = poles[idx];
|
if (poles[idx]._marker) poleLayer.removeLayer(poles[idx]._marker);
|
||||||
if (pole._marker) poleLayer.removeLayer(pole._marker);
|
|
||||||
poles.splice(idx, 1);
|
poles.splice(idx, 1);
|
||||||
|
renumberPoles();
|
||||||
|
}
|
||||||
|
|
||||||
|
function removePoleFromTable(id) {
|
||||||
|
saveHistory();
|
||||||
|
removePoleById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renumberPoles() {
|
||||||
poles.forEach((p, i) => {
|
poles.forEach((p, i) => {
|
||||||
p.id = i + 1;
|
p.id = i + 1;
|
||||||
p.label = `Опора ${i + 1}`;
|
p.label = `Опора ${i + 1}`;
|
||||||
@ -200,6 +310,7 @@ function removePole(id) {
|
|||||||
poles.forEach((p) => renderPole(p));
|
poles.forEach((p) => renderPole(p));
|
||||||
updateTable();
|
updateTable();
|
||||||
updateConnections();
|
updateConnections();
|
||||||
|
updateUndoButtons();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateConnections() {
|
function updateConnections() {
|
||||||
@ -210,11 +321,8 @@ function updateConnections() {
|
|||||||
for (let i = 0; i < sorted.length - 1; i++) {
|
for (let i = 0; i < sorted.length - 1; i++) {
|
||||||
const a = sorted[i];
|
const a = sorted[i];
|
||||||
const b = sorted[i + 1];
|
const b = sorted[i + 1];
|
||||||
const polyline = L.polyline(
|
L.polyline(
|
||||||
[
|
[[a.lat, a.lng], [b.lat, b.lng]],
|
||||||
[a.lat, a.lng],
|
|
||||||
[b.lat, b.lng],
|
|
||||||
],
|
|
||||||
{ color: "#64748b", weight: 2, dashArray: "6,4", opacity: 0.7 }
|
{ color: "#64748b", weight: 2, dashArray: "6,4", opacity: 0.7 }
|
||||||
).addTo(connectionLayer);
|
).addTo(connectionLayer);
|
||||||
|
|
||||||
@ -224,11 +332,7 @@ function updateConnections() {
|
|||||||
L.marker([midLat, midLng], {
|
L.marker([midLat, midLng], {
|
||||||
icon: L.divIcon({
|
icon: L.divIcon({
|
||||||
className: "dist-label",
|
className: "dist-label",
|
||||||
html: `<div style="
|
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>`,
|
||||||
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],
|
iconSize: [0, 0],
|
||||||
iconAnchor: [0, 0],
|
iconAnchor: [0, 0],
|
||||||
}),
|
}),
|
||||||
@ -241,27 +345,27 @@ function updateTable() {
|
|||||||
const tbody = document.querySelector("#poleTable tbody");
|
const tbody = document.querySelector("#poleTable tbody");
|
||||||
tbody.innerHTML = "";
|
tbody.innerHTML = "";
|
||||||
if (poles.length === 0) {
|
if (poles.length === 0) {
|
||||||
tbody.innerHTML = '<tr><td colspan="6" class="text-center text-gray-500 py-4">Нет опор. Кликните на карту, чтобы добавить.</td></tr>';
|
tbody.innerHTML = '<tr><td colspan="7" class="text-center text-gray-500 py-4">Нет опор. Кликните на карту, чтобы добавить.</td></tr>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sorted = [...poles].sort((a, b) => a.id - b.id);
|
const sorted = [...poles].sort((a, b) => a.id - b.id);
|
||||||
sorted.forEach((p, i) => {
|
sorted.forEach((p, i) => {
|
||||||
const type = POLE_TYPES[p.type];
|
const type = POLE_TYPES[p.type];
|
||||||
const prevDist =
|
const prevDist = i > 0 ? calcDistance(sorted[i - 1].lat, sorted[i - 1].lng, p.lat, p.lng).toFixed(1) : "—";
|
||||||
i > 0
|
const statusLabel = p.status === "existing" ? `Сущ. (${EXISTING_TYPES[p.existingType] || p.existingType})` : "Проект.";
|
||||||
? calcDistance(sorted[i - 1].lat, sorted[i - 1].lng, p.lat, p.lng).toFixed(1)
|
const statusColor = p.status === "existing" ? "#f59e0b" : "#10b981";
|
||||||
: "—";
|
|
||||||
const tr = document.createElement("tr");
|
const tr = document.createElement("tr");
|
||||||
tr.className = "border-b border-gray-200 hover:bg-gray-50";
|
tr.className = "border-b border-gray-200 hover:bg-gray-50";
|
||||||
tr.innerHTML = `
|
tr.innerHTML = `
|
||||||
<td class="px-4 py-2 font-medium">${p.id}</td>
|
<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"><span class="pole-badge" style="background:${type.color}">${type.name}</span></td>
|
||||||
|
<td class="px-4 py-2"><span class="pole-badge" style="background:${statusColor}">${statusLabel}</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.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 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-right font-mono">${prevDist}</td>
|
||||||
<td class="px-4 py-2 text-center">
|
<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>
|
<button onclick="removePoleFromTable(${p.id})" class="text-red-500 hover:text-red-700 text-sm" title="Удалить">✕</button>
|
||||||
</td>
|
</td>
|
||||||
`;
|
`;
|
||||||
tbody.appendChild(tr);
|
tbody.appendChild(tr);
|
||||||
@ -272,22 +376,21 @@ function calcDistance(lat1, lon1, lat2, lon2) {
|
|||||||
const R = 6371000;
|
const R = 6371000;
|
||||||
const dLat = ((lat2 - lat1) * Math.PI) / 180;
|
const dLat = ((lat2 - lat1) * Math.PI) / 180;
|
||||||
const dLon = ((lon2 - lon1) * Math.PI) / 180;
|
const dLon = ((lon2 - lon1) * Math.PI) / 180;
|
||||||
const a =
|
const a = Math.sin(dLat / 2) ** 2 + Math.cos((lat1 * Math.PI) / 180) * Math.cos((lat2 * Math.PI) / 180) * Math.sin(dLon / 2) ** 2;
|
||||||
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));
|
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearAll() {
|
function clearAll() {
|
||||||
if (poles.length === 0) return;
|
if (poles.length === 0) return;
|
||||||
if (!confirm("Удалить все опоры?")) return;
|
if (!confirm("Удалить все опоры?")) return;
|
||||||
|
saveHistory();
|
||||||
poleLayer.clearLayers();
|
poleLayer.clearLayers();
|
||||||
connectionLayer.clearLayers();
|
connectionLayer.clearLayers();
|
||||||
poles = [];
|
poles = [];
|
||||||
poleCounter = 1;
|
poleCounter = 1;
|
||||||
updateTable();
|
updateTable();
|
||||||
|
updateConnections();
|
||||||
|
updateUndoButtons();
|
||||||
}
|
}
|
||||||
|
|
||||||
function exportSVG() {
|
function exportSVG() {
|
||||||
@ -303,65 +406,56 @@ function exportSVG() {
|
|||||||
const latRange = maxLat - minLat || 0.001;
|
const latRange = maxLat - minLat || 0.001;
|
||||||
const lngRange = maxLng - minLng || 0.001;
|
const lngRange = maxLng - minLng || 0.001;
|
||||||
|
|
||||||
const scale = 500 / Math.max(latRange, lngRange);
|
|
||||||
const toX = (lng) => padding + ((lng - minLng) / lngRange) * 500;
|
const toX = (lng) => padding + ((lng - minLng) / lngRange) * 500;
|
||||||
const toY = (lat) => padding + ((maxLat - lat) / latRange) * 500;
|
const toY = (lat) => padding + ((maxLat - lat) / latRange) * 500;
|
||||||
const w = 500 + padding * 2;
|
const w = 500 + padding * 2;
|
||||||
const h = 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);
|
const sorted = [...poles].sort((a, b) => a.id - b.id);
|
||||||
|
|
||||||
// connections
|
let svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${w} ${h}" width="${w}" height="${h}">
|
||||||
|
<style>
|
||||||
|
.lbl{font:bold 10px sans-serif;text-anchor:middle;fill:#fff}
|
||||||
|
.dl{font:9px sans-serif;text-anchor:middle;fill:#333}
|
||||||
|
.tl{font:14px sans-serif;font-weight:700;fill:#333}
|
||||||
|
.ll{font:9px sans-serif;fill:#555}
|
||||||
|
</style>
|
||||||
|
<rect width="100%" height="100%" fill="#f8f9fa"/>
|
||||||
|
<text x="${w/2}" y="24" text-anchor="middle" class="tl">Схема расположения опор</text>`;
|
||||||
|
|
||||||
for (let i = 0; i < sorted.length - 1; i++) {
|
for (let i = 0; i < sorted.length - 1; i++) {
|
||||||
const a = sorted[i],
|
const a = sorted[i], b = sorted[i + 1];
|
||||||
b = sorted[i + 1];
|
|
||||||
const dist = calcDistance(a.lat, a.lng, b.lat, b.lng).toFixed(1);
|
const dist = calcDistance(a.lat, a.lng, b.lat, b.lng).toFixed(1);
|
||||||
const mx = (toX(a.lng) + toX(b.lng)) / 2;
|
const mx = (toX(a.lng) + toX(b.lng)) / 2;
|
||||||
const my = (toY(a.lat) + toY(b.lat)) / 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 += `<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>`;
|
svg += `<text x="${mx}" y="${my-6}" class="dl">${dist} м</text>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// poles
|
|
||||||
sorted.forEach((p) => {
|
sorted.forEach((p) => {
|
||||||
const type = POLE_TYPES[p.type];
|
const type = POLE_TYPES[p.type];
|
||||||
const cx = toX(p.lng);
|
const cx = toX(p.lng), cy = toY(p.lat);
|
||||||
const cy = toY(p.lat);
|
|
||||||
svg += `<circle cx="${cx}" cy="${cy}" r="12" fill="${type.color}" stroke="#fff" stroke-width="2"/>`;
|
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}" y="${cy+3}" class="lbl">${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>`;
|
const stLabel = p.status === "existing" ? `сущ.${EXISTING_TYPES[p.existingType]}` : "проект.";
|
||||||
|
svg += `<text x="${cx+18}" y="${cy+3}" class="ll">${p.label} (${type.name}, ${stLabel})</text>`;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Legend
|
const legY = h - 40;
|
||||||
const legendY = h - 40;
|
svg += `<rect x="${padding}" y="${legY-10}" width="${w-padding*2}" height="2" fill="#e2e8f0"/>`;
|
||||||
svg += `<rect x="${padding}" y="${legendY - 10}" width="${w - padding * 2}" height="2" fill="#e2e8f0"/>`;
|
|
||||||
let lx = padding;
|
let lx = padding;
|
||||||
Object.entries(POLE_TYPES).forEach(([k, t]) => {
|
Object.entries(POLE_TYPES).forEach(([, t]) => {
|
||||||
svg += `<circle cx="${lx + 6}" cy="${legendY + 10}" r="6" fill="${t.color}" stroke="#ccc" stroke-width="1"/>`;
|
svg += `<circle cx="${lx+6}" cy="${legY+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>`;
|
svg += `<text x="${lx+16}" y="${legY+14}" class="ll">${t.name}</text>`;
|
||||||
lx += 140;
|
lx += 140;
|
||||||
});
|
});
|
||||||
|
|
||||||
svg += `</svg>`;
|
svg += `</svg>`;
|
||||||
|
|
||||||
downloadFile(svg, "opory.svg", "image/svg+xml");
|
downloadFile(svg, "opory.svg", "image/svg+xml");
|
||||||
}
|
}
|
||||||
|
|
||||||
function exportVSD() {
|
function exportVSD() {
|
||||||
alert(
|
alert('VSD (Visio) — бинарный формат.\n\nЭкспортируйте в SVG, затем откройте SVG в Visio: Файл → Открыть → выберите .svg\nИли перетащите SVG-файл в окно Visio.');
|
||||||
"VSD (Visio) — бинарный формат. SVG можно открыть в Visio.\n\n" +
|
|
||||||
"Экспортируйте в SVG, затем откройте в Visio: Файл → Открыть → выберите .svg\n" +
|
|
||||||
"Или перетащите SVG-файл в окно Visio."
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function downloadFile(content, filename, mime) {
|
function downloadFile(content, filename, mime) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user