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-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-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:focus,select:focus{border-color:#0f172a}
|
||||
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">
|
||||
<!-- Sidebar -->
|
||||
<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>
|
||||
<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 class="flex-1 overflow-y-auto scrollbar-thin px-5 py-4 space-y-5">
|
||||
<!-- Search -->
|
||||
<div class="relative">
|
||||
<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>
|
||||
|
||||
<!-- Selectors -->
|
||||
<div class="space-y-3">
|
||||
<div class="space-y-2">
|
||||
<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>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
|
||||
<!-- Pole types -->
|
||||
<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">
|
||||
<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="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="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>
|
||||
<p class="text-xs text-gray-400 mt-2">Кликните на карту, чтобы поставить опору выбранного типа</p>
|
||||
<p class="text-xs text-gray-400 mt-2">Кликните на карту, чтобы поставить опору</p>
|
||||
</div>
|
||||
|
||||
<!-- 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-secondary flex-1" onclick="exportVSD()">📥 VSD</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>
|
||||
<th class="w-14">№</th>
|
||||
<th>Тип</th>
|
||||
<th>Статус</th>
|
||||
<th>Широта</th>
|
||||
<th>Долгота</th>
|
||||
<th class="text-right">Дист. до пред.</th>
|
||||
@ -112,7 +156,7 @@ table td{padding:10px 16px;border-bottom:1px solid #f1f5f9}
|
||||
</tr>
|
||||
</thead>
|
||||
<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>
|
||||
</table>
|
||||
</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 poleCounter = 1;
|
||||
let currentPoleType = "1";
|
||||
let currentPoleStatus = "projected";
|
||||
let currentExistingType = "communication";
|
||||
let history = [];
|
||||
let historyIndex = -1;
|
||||
|
||||
const POLE_TYPES = {
|
||||
"1": { name: "Промежуточная", color: "#2563eb", icon: "●" },
|
||||
@ -11,27 +15,82 @@ const POLE_TYPES = {
|
||||
"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() {
|
||||
initMap();
|
||||
initSelectors();
|
||||
initPoleControls();
|
||||
updateTable();
|
||||
updateUndoButtons();
|
||||
}
|
||||
|
||||
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);
|
||||
tileLayer = L.tileLayer(TILE_PROVIDERS.street.url, { attribution: TILE_PROVIDERS.street.attr, maxZoom: 19 }).addTo(map);
|
||||
|
||||
drawnItems = L.featureGroup().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() {
|
||||
@ -39,6 +98,7 @@ function initSelectors() {
|
||||
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");
|
||||
@ -55,10 +115,9 @@ function initSelectors() {
|
||||
oblast.rayons.forEach((r) => {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = r.rayon;
|
||||
opt.textContent = r.rayon;
|
||||
opt.textContent = `${r.rayon} (${r.villages.length} сёл)`;
|
||||
rayonSel.appendChild(opt);
|
||||
});
|
||||
updateVillages();
|
||||
}
|
||||
|
||||
function updateVillages() {
|
||||
@ -78,20 +137,36 @@ function initSelectors() {
|
||||
oblastSel.addEventListener("change", updateRayons);
|
||||
rayonSel.addEventListener("change", updateVillages);
|
||||
|
||||
villageSel.addEventListener("change", function () {
|
||||
const name = this.value;
|
||||
function goToVillage(name) {
|
||||
if (!name) return;
|
||||
const oblast = KZ_DATA.find((o) => o.oblast === oblastSel.value);
|
||||
if (!oblast) return;
|
||||
map.setView(oblast.center, 10);
|
||||
map.setView(oblast.center, 12);
|
||||
L.popup()
|
||||
.setLatLng(oblast.center)
|
||||
.setContent(`<b>${name}</b><br>${oblast.oblast}, ${rayonSel.value}`)
|
||||
.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) => {
|
||||
@ -103,13 +178,11 @@ function initSelectors() {
|
||||
});
|
||||
});
|
||||
});
|
||||
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>";
|
||||
if (results.length === 0) {
|
||||
list.innerHTML = "<li class='px-3 py-2 text-gray-500'>Не найдено</li>";
|
||||
return;
|
||||
}
|
||||
results.slice(0, 20).forEach((r) => {
|
||||
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}`;
|
||||
@ -147,16 +220,42 @@ function initPoleControls() {
|
||||
});
|
||||
});
|
||||
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, label: `Опора ${id}` };
|
||||
const pole = { id, lat, lng, type: currentPoleType, status: currentPoleStatus, existingType: currentExistingType, label: `Опора ${id}` };
|
||||
poles.push(pole);
|
||||
renderPole(pole);
|
||||
updateTable();
|
||||
updateConnections();
|
||||
updateUndoButtons();
|
||||
}
|
||||
|
||||
function renderPole(pole) {
|
||||
@ -176,21 +275,32 @@ function renderPole(pole) {
|
||||
});
|
||||
|
||||
const marker = L.marker([pole.lat, pole.lng], { icon }).addTo(poleLayer);
|
||||
const statusLabel = pole.status === "existing" ? `Существующая (${EXISTING_TYPES[pole.existingType] || pole.existingType})` : "Проектируемая";
|
||||
marker.bindPopup(`
|
||||
<b>${pole.label}</b><br>
|
||||
Тип: ${type.name}<br>
|
||||
Статус: ${statusLabel}<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>
|
||||
<br><button onclick="removePoleById(${pole.id})" style="color:red;cursor:pointer;border:none;background:none;margin-top:4px">✕ Удалить</button>
|
||||
`);
|
||||
pole._marker = marker;
|
||||
}
|
||||
|
||||
function removePole(id) {
|
||||
function removePoleById(id) {
|
||||
saveHistory();
|
||||
const idx = poles.findIndex((p) => p.id === id);
|
||||
if (idx === -1) return;
|
||||
const pole = poles[idx];
|
||||
if (pole._marker) poleLayer.removeLayer(pole._marker);
|
||||
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}`;
|
||||
@ -200,6 +310,7 @@ function removePole(id) {
|
||||
poles.forEach((p) => renderPole(p));
|
||||
updateTable();
|
||||
updateConnections();
|
||||
updateUndoButtons();
|
||||
}
|
||||
|
||||
function updateConnections() {
|
||||
@ -210,11 +321,8 @@ function updateConnections() {
|
||||
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],
|
||||
],
|
||||
L.polyline(
|
||||
[[a.lat, a.lng], [b.lat, b.lng]],
|
||||
{ color: "#64748b", weight: 2, dashArray: "6,4", opacity: 0.7 }
|
||||
).addTo(connectionLayer);
|
||||
|
||||
@ -224,11 +332,7 @@ function updateConnections() {
|
||||
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>`,
|
||||
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],
|
||||
}),
|
||||
@ -241,27 +345,27 @@ function updateTable() {
|
||||
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>';
|
||||
tbody.innerHTML = '<tr><td colspan="7" 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 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 = `
|
||||
<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:${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.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>
|
||||
<button onclick="removePoleFromTable(${p.id})" class="text-red-500 hover:text-red-700 text-sm" title="Удалить">✕</button>
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
@ -272,22 +376,21 @@ 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;
|
||||
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() {
|
||||
@ -303,65 +406,56 @@ function exportSVG() {
|
||||
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
|
||||
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++) {
|
||||
const a = sorted[i],
|
||||
b = sorted[i + 1];
|
||||
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>`;
|
||||
svg += `<text x="${mx}" y="${my-6}" class="dl">${dist} м</text>`;
|
||||
}
|
||||
|
||||
// poles
|
||||
sorted.forEach((p) => {
|
||||
const type = POLE_TYPES[p.type];
|
||||
const cx = toX(p.lng);
|
||||
const cy = toY(p.lat);
|
||||
const cx = toX(p.lng), 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>`;
|
||||
svg += `<text x="${cx}" y="${cy+3}" class="lbl">${p.id}</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 legendY = h - 40;
|
||||
svg += `<rect x="${padding}" y="${legendY - 10}" width="${w - padding * 2}" height="2" fill="#e2e8f0"/>`;
|
||||
const legY = h - 40;
|
||||
svg += `<rect x="${padding}" y="${legY-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>`;
|
||||
Object.entries(POLE_TYPES).forEach(([, t]) => {
|
||||
svg += `<circle cx="${lx+6}" cy="${legY+10}" r="6" fill="${t.color}" stroke="#ccc" stroke-width="1"/>`;
|
||||
svg += `<text x="${lx+16}" y="${legY+14}" class="ll">${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."
|
||||
);
|
||||
alert('VSD (Visio) — бинарный формат.\n\nЭкспортируйте в SVG, затем откройте SVG в Visio: Файл → Открыть → выберите .svg\nИли перетащите SVG-файл в окно Visio.');
|
||||
}
|
||||
|
||||
function downloadFile(content, filename, mime) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user