+
@@ -105,6 +148,7 @@ table td{padding:10px 16px;border-bottom:1px solid #f1f5f9}
| № |
Тип |
+ Статус |
Широта |
Долгота |
Дист. до пред. |
@@ -112,7 +156,7 @@ table td{padding:10px 16px;border-bottom:1px solid #f1f5f9}
- | Кликните на карту, чтобы добавить опору |
+ | Кликните на карту, чтобы добавить опору |
diff --git a/js/app.js b/js/app.js
index 3a14a6a..66f4be2 100644
--- a/js/app.js
+++ b/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: "©
CARTO" },
+ 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(`
${name}${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 = "
Слишком много результатов, уточните";
+ if (results.length === 0) {
+ list.innerHTML = "
Не найдено";
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(`
${pole.label}
Тип: ${type.name}
+ Статус: ${statusLabel}
Координаты: ${pole.lat.toFixed(6)}, ${pole.lng.toFixed(6)}
-
+
`);
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: `
${dist.toFixed(1)} м
`,
+ html: `
${dist.toFixed(1)} м
`,
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 = '
| Нет опор. Кликните на карту, чтобы добавить. |
';
+ 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 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 = `
${p.id} |
${type.name} |
+
${statusLabel} |
${p.lat.toFixed(6)} |
${p.lng.toFixed(6)} |
${prevDist} |
-
+
|
`;
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 = `