v2: semi-auto placement, entrances, line mode
This commit is contained in:
parent
bfc6712977
commit
1b51bb3b8e
265
index.html
265
index.html
@ -3,104 +3,219 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
<title>Планировщик опор</title>
|
<title>Планировщик опор ВЛ-0.4 кВ</title>
|
||||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
|
||||||
<style>
|
<style>
|
||||||
:root{--ink:#0F1218;--cyan:#00E5FF;--white:#fff;--gray-100:#F2F4F7;--gray-500:#5B6573}
|
:root{--ink:#0F1218;--cyan:#00E5FF;--white:#fff;--gray-100:#F2F4F7;--gray-500:#5B6573}
|
||||||
*{box-sizing:border-box;margin:0;padding:0}
|
*{box-sizing:border-box;margin:0;padding:0}
|
||||||
body{font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Inter,sans-serif;color:var(--ink);background:var(--gray-100)}
|
body{font:14px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Inter,sans-serif;color:var(--ink);background:var(--gray-100)}
|
||||||
.header{background:var(--ink);color:var(--white);padding:16px 24px;display:flex;align-items:center;gap:16px;flex-wrap:wrap}
|
.header{background:var(--ink);color:var(--white);padding:10px 16px;display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
||||||
.header h1{font-size:20px;font-weight:700}
|
.header h1{font-size:16px;font-weight:700;white-space:nowrap}
|
||||||
.header .badge{background:var(--cyan);color:var(--ink);padding:4px 10px;border-radius:12px;font-size:12px;font-weight:700}
|
.controls{display:flex;gap:4px;flex-wrap:wrap}
|
||||||
.controls{display:flex;gap:8px;flex-wrap:wrap}
|
.controls button{padding:5px 10px;border:none;border-radius:4px;font-weight:600;cursor:pointer;font-size:11px;white-space:nowrap}
|
||||||
.controls button{padding:8px 16px;border:none;border-radius:6px;font-weight:600;cursor:pointer;font-size:13px}
|
.btn-line{background:#00E5FF;color:#0F1218}
|
||||||
.btn-add{background:var(--cyan);color:var(--ink)}
|
.btn-add{background:#4CAF50;color:#fff}
|
||||||
|
.btn-driveway{background:#FF9800;color:#fff}
|
||||||
.btn-undo{background:#ff5252;color:#fff}
|
.btn-undo{background:#ff5252;color:#fff}
|
||||||
.btn-export{background:#4caf50;color:#fff}
|
.btn-export{background:#4caf50;color:#fff}
|
||||||
.btn-clear{background:#ff9800;color:#fff}
|
.btn-clear{background:#ff9800;color:#fff}
|
||||||
#map{height:calc(100vh - 200px);min-height:400px}
|
.btn-active{background:#fff!important;color:#0F1218!important;box-shadow:0 0 0 2px var(--cyan)}
|
||||||
.sidebar{position:absolute;top:70px;right:16px;width:320px;max-height:calc(100vh - 100px);background:var(--white);border-radius:12px;box-shadow:0 4px 24px rgba(0,0,0,.15);overflow:hidden;z-index:1000;display:flex;flex-direction:column}
|
#map{height:calc(100vh - 176px);min-height:350px}
|
||||||
.sidebar-header{padding:12px 16px;background:var(--gray-100);font-weight:700;font-size:14px;border-bottom:1px solid #ddd}
|
.status{margin:4px 12px 0;font-size:11px;color:#9aa3b2;display:flex;gap:16px;flex-wrap:wrap}
|
||||||
.sidebar table{width:100%;border-collapse:collapse;font-size:13px}
|
.status span{margin-right:12px}
|
||||||
.sidebar th{background:var(--ink);color:var(--white);padding:8px 10px;text-align:left}
|
.sidebar{position:absolute;top:56px;right:8px;width:320px;max-height:calc(100vh - 64px);background:var(--white);border-radius:8px;box-shadow:0 4px 20px rgba(0,0,0,.15);overflow:hidden;z-index:1000;display:flex;flex-direction:column;font-size:12px}
|
||||||
.sidebar td{padding:6px 10px;border-bottom:1px solid var(--gray-100)}
|
.sidebar-header{padding:8px 12px;background:var(--gray-100);font-weight:700;font-size:12px;border-bottom:1px solid #ddd;display:flex;justify-content:space-between;align-items:center}
|
||||||
.sidebar td.num{text-align:center;font-weight:700;color:var(--cyan)}
|
.sidebar table{width:100%;border-collapse:collapse;font-size:11px}
|
||||||
.sidebar td.dist{text-align:right;font-family:monospace}
|
.sidebar th{background:var(--ink);color:var(--white);padding:4px 6px;text-align:left;position:sticky;top:0;font-size:11px}
|
||||||
.sidebar-footer{padding:10px 16px;background:var(--gray-100);font-size:13px;font-weight:600;border-top:1px solid #ddd}
|
.sidebar td{padding:3px 6px;border-bottom:1px solid var(--gray-100)}
|
||||||
.pole-label{background:var(--ink);color:var(--cyan);border-radius:50%;width:28px;height:28px;display:flex;align-items:center;justify-content:center;font-weight:800;font-size:13px;border:2px solid var(--cyan);box-shadow:0 2px 8px rgba(0,0,0,.3)}
|
.sidebar td.num{text-align:center;font-weight:700;color:var(--cyan);font-size:11px}
|
||||||
.mode-badge{position:absolute;top:80px;left:16px;background:var(--cyan);color:var(--ink);padding:6px 14px;border-radius:8px;font-weight:700;font-size:13px;z-index:1000;pointer-events:none}
|
.sidebar td.dist{text-align:right;font-family:monospace;font-size:11px}
|
||||||
.toast{position:fixed;bottom:20px;left:50%;transform:translateX(-50%);background:var(--ink);color:var(--white);padding:10px 20px;border-radius:8px;font-size:13px;z-index:9999;opacity:0;transition:opacity .3s}
|
.sidebar-footer{padding:6px 12px;background:var(--gray-100);font-size:11px;font-weight:600;border-top:1px solid #ddd}
|
||||||
|
.pole-label{background:#0F1218;color:#00E5FF;border-radius:50%;width:24px;height:24px;display:flex;align-items:center;justify-content:center;font-weight:800;font-size:11px;border:2px solid #00E5FF;box-shadow:0 2px 6px rgba(0,0,0,.4)}
|
||||||
|
.entrance-label{background:#FF9800;color:#fff;border-radius:4px;padding:2px 6px;font-size:10px;font-weight:700;white-space:nowrap;border:2px solid #FF9800;box-shadow:0 2px 6px rgba(0,0,0,.3)}
|
||||||
|
.mode-badge{position:absolute;top:56px;left:8px;background:var(--cyan);color:var(--ink);padding:4px 10px;border-radius:4px;font-weight:700;font-size:11px;z-index:1000;pointer-events:none}
|
||||||
|
.legend{position:absolute;bottom:16px;left:8px;background:var(--white);padding:6px 10px;border-radius:6px;box-shadow:0 2px 10px rgba(0,0,0,.1);z-index:1000;font-size:11px;line-height:1.6}
|
||||||
|
.legend span{display:flex;align-items:center;gap:6px}
|
||||||
|
.toast{position:fixed;bottom:12px;left:50%;transform:translateX(-50%);background:#0F1218;color:#fff;padding:6px 14px;border-radius:4px;font-size:11px;z-index:9999;opacity:0;transition:opacity .3s}
|
||||||
.toast.show{opacity:1}
|
.toast.show{opacity:1}
|
||||||
@media(max-width:700px){.sidebar{width:100%;right:0;top:auto;bottom:0;max-height:45vh;border-radius:12px 12px 0 0}#map{height:55vh}}
|
.settings{display:flex;gap:4px;align-items:center;margin:0 12px 4px;font-size:11px;color:#9aa3b2;flex-wrap:wrap}
|
||||||
|
.settings label{display:flex;align-items:center;gap:4px}
|
||||||
|
.settings input{width:45px;padding:2px 4px;border:1px solid #555;border-radius:3px;background:#222;color:#fff;font-size:11px;text-align:center}
|
||||||
|
@media(max-width:700px){.sidebar{width:100%;right:0;top:auto;bottom:0;max-height:35vh;border-radius:8px 8px 0 0}#map{height:50vh}}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h1>Планировщик опор</h1>
|
<h1>Опоры ВЛ-0.4 кВ</h1>
|
||||||
<span class="badge">Google Maps</span>
|
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<button class="btn-add" id="btnAdd" onclick="toggleMode()">+ Добавить опору</button>
|
<button class="btn-line" id="btnLine" onclick="setMode('line')">📏 Линия</button>
|
||||||
<button class="btn-undo" onclick="undo()">↩ Отменить</button>
|
<button class="btn-active" id="btnManual" onclick="setMode('manual')">🎯 Вручную</button>
|
||||||
<button class="btn-clear" onclick="clearAll()">🗑 Очистить</button>
|
<button class="btn-driveway" id="btnEntrance" onclick="setMode('entrance')">🚗 Въезд</button>
|
||||||
|
<button class="btn-undo" onclick="undo()">↩</button>
|
||||||
|
<button class="btn-clear" onclick="clearAll()">🗑</button>
|
||||||
<button class="btn-export" onclick="exportCSV()">📥 CSV</button>
|
<button class="btn-export" onclick="exportCSV()">📥 CSV</button>
|
||||||
<button class="btn-export" onclick="exportJSON()">📥 JSON</button>
|
<button class="btn-export" onclick="exportJSON()">📥 JSON</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="settings">
|
||||||
|
<label>📐 Шаг опор: <input type="number" id="poleStep" value="50" min="10" max="200"> м</label>
|
||||||
|
<label>⛔ Отступ от въезда: <input type="number" id="entranceMargin" value="5" min="0" max="30"> м</label>
|
||||||
|
<span id="modeInfo" style="margin-left:auto">Режим: ручная расстановка</span>
|
||||||
|
</div>
|
||||||
<div id="map"></div>
|
<div id="map"></div>
|
||||||
<div class="mode-badge" id="modeBadge" style="display:none">🎯 Кликните на карту, чтобы поставить опору</div>
|
<div class="mode-badge" id="modeBadge" style="display:none">🎯 Кликните для установки</div>
|
||||||
|
<div class="legend">
|
||||||
|
<span><span style="display:inline-block;width:12px;height:12px;border-radius:50%;background:#0F1218;border:2px solid #00E5FF"></span> Опора ЖБ</span>
|
||||||
|
<span><i style="display:inline-block;width:14px;height:4px;background:#00E5FF;border-radius:2px"></i> Линия ВЛ</span>
|
||||||
|
<span><span style="display:inline-block;background:#FF9800;padding:0 4px;border-radius:2px;color:#fff;font-size:9px">В</span> Въезд во двор</span>
|
||||||
|
</div>
|
||||||
<div class="sidebar">
|
<div class="sidebar">
|
||||||
<div class="sidebar-header">Таблица опор</div>
|
<div class="sidebar-header"><span>📋 Спецификация опор</span><span id="stats" style="font-weight:400;font-size:11px">0 шт</span></div>
|
||||||
<div style="overflow-y:auto;flex:1">
|
<div style="overflow-y:auto;flex:1">
|
||||||
<table>
|
<table>
|
||||||
<thead><tr><th>№</th><th>Название</th><th>Широта</th><th>Долгота</th><th>Дистанция</th></tr></thead>
|
<thead><tr><th>№</th><th>Тип</th><th>Широта</th><th>Долгота</th><th>К линии</th></tr></thead>
|
||||||
<tbody id="poleTable"></tbody>
|
<tbody id="poleTable"></tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="sidebar-footer" id="totalDist">Итого: 0 м (0 опор)</div>
|
<div class="sidebar-footer" id="totalDist">Итого: 0.0 м | 0 опор</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="toast" id="toast"></div>
|
<div class="toast" id="toast"></div>
|
||||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||||
<script>
|
<script>
|
||||||
let map, markers=[], lines=[], addMode=false, poleCount=0;
|
let map, markers=[], lines=[], entrances=[], entranceCircles=[];
|
||||||
const LAYER_SATELLITE='https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}';
|
let poleCount=0, linePoints=[], mode='manual', addingLine=false;
|
||||||
const LAYER_LABELS='https://server.arcgisonline.com/ArcGIS/rest/services/Reference/World_Boundaries_and_Places/MapServer/tile/{z}/{y}/{x}';
|
|
||||||
|
|
||||||
function init(){
|
function init(){
|
||||||
map=L.map('map',{zoomControl:true}).setView([43.238,76.946],15);
|
const sat=L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',{maxZoom:20,attribution:'Esri'});
|
||||||
L.tileLayer(LAYER_SATELLITE,{maxZoom:19,attribution:'Esri'}).addTo(map);
|
const osm=L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png',{maxZoom:20,opacity:0.45,attribution:'OSM'});
|
||||||
L.tileLayer(LAYER_LABELS,{maxZoom:19,opacity:0.7}).addTo(map);
|
map=L.map('map',{zoomControl:true,layers:[sat,osm]}).setView([43.238,76.946],16);
|
||||||
map.on('click',onMapClick);
|
map.on('click',onMapClick);
|
||||||
|
updateModeBadge();
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleMode(){
|
function setMode(newMode){
|
||||||
addMode=!addMode;
|
mode=newMode;
|
||||||
document.getElementById('btnAdd').textContent=addMode?'✕ Отмена':'+ Добавить опору';
|
document.getElementById('btnLine').className='btn-line';
|
||||||
document.getElementById('modeBadge').style.display=addMode?'block':'none';
|
document.getElementById('btnManual').className='btn-add';
|
||||||
|
document.getElementById('btnEntrance').className='btn-driveway';
|
||||||
|
const el=document.getElementById('btn'+newMode.charAt(0).toUpperCase()+newMode.slice(1));
|
||||||
|
if(el)el.className+=' btn-active';
|
||||||
|
if(mode==='line')addingLine=false;
|
||||||
|
if(mode==='line')linePoints=[];
|
||||||
|
document.getElementById('modeInfo').textContent={
|
||||||
|
line:'Режим: рисуем линию улицы → опоры автоматом',
|
||||||
|
manual:'Режим: ручная расстановка',
|
||||||
|
entrance:'Режим: отметить въезд во двор'
|
||||||
|
}[mode];
|
||||||
|
updateModeBadge();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateModeBadge(){
|
||||||
|
const b=document.getElementById('modeBadge');
|
||||||
|
if(mode==='manual'){b.style.display='block';b.innerHTML='🎯 Кликните для опоры';}
|
||||||
|
else if(mode==='line'){b.style.display='block';b.innerHTML='📏 Кликните точки линии улицы<br><small>Enter — расставить опоры</small>';}
|
||||||
|
else if(mode==='entrance'){b.style.display='block';b.innerHTML='🚗 Кликните на въезде во двор';}
|
||||||
|
else b.style.display='none';
|
||||||
}
|
}
|
||||||
|
|
||||||
function onMapClick(e){
|
function onMapClick(e){
|
||||||
if(!addMode)return;
|
if(mode==='manual'){addPole(e.latlng);return;}
|
||||||
|
if(mode==='entrance'){addEntrance(e.latlng);return;}
|
||||||
|
if(mode==='line'){addLinePoint(e.latlng);return;}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addPole(latlng){
|
||||||
poleCount++;
|
poleCount++;
|
||||||
const name='Опора '+poleCount;
|
const name='Опора '+poleCount;
|
||||||
const icon=L.divIcon({className:'',html:'<div class="pole-label">'+poleCount+'</div>',iconSize:[28,28],iconAnchor:[14,14]});
|
const icon=L.divIcon({className:'',html:'<div class="pole-label">'+poleCount+'</div>',iconSize:[24,24],iconAnchor:[12,12]});
|
||||||
const marker=L.marker(e.latlng,{icon}).addTo(map).bindPopup('<b>'+name+'</b><br>'+e.latlng.lat.toFixed(6)+', '+e.latlng.lng.toFixed(6));
|
const marker=L.marker(latlng,{icon}).addTo(map).bindPopup('<b>'+name+'</b>');
|
||||||
markers.push({id:poleCount,lat:e.latlng.lat,lng:e.latlng.lng,name,marker});
|
markers.push({id:poleCount,lat:latlng.lat,lng:latlng.lng,name,marker});
|
||||||
|
updateTable();
|
||||||
if(markers.length>1){
|
if(markers.length>1){
|
||||||
const prev=markers[markers.length-2];
|
const p=markers[markers.length-2];
|
||||||
const dist=haversine(prev.lat,prev.lng,e.latlng.lat,e.latlng.lng);
|
const dist=haversine(p.lat,p.lng,latlng.lat,latlng.lng);
|
||||||
const line=L.polyline([[prev.lat,prev.lng],[e.latlng.lat,e.latlng.lng]],{color:'#00E5FF',weight:3,dashArray:'8,6'}).addTo(map);
|
const line=L.polyline([[p.lat,p.lng],[latlng.lat,latlng.lng]],{color:'#00E5FF',weight:2.5,dashArray:'6,5'}).addTo(map);
|
||||||
lines.push(line);
|
lines.push(line);
|
||||||
}
|
}
|
||||||
updateTable();
|
showToast('✅ '+name);
|
||||||
showToast(name+' добавлена');
|
}
|
||||||
|
|
||||||
|
function addLinePoint(latlng){
|
||||||
|
linePoints.push(latlng);
|
||||||
|
if(linePoints.length>1){
|
||||||
|
const p=linePoints[linePoints.length-2];
|
||||||
|
L.polyline([[p.lat,p.lng],[latlng.lat,latlng.lng]],{color:'#FF9800',weight:2,dashArray:'4,4',opacity:0.6}).addTo(map)._tmpLine=true;
|
||||||
|
}
|
||||||
|
L.circleMarker(latlng,{radius:4,color:'#FF9800',fillColor:'#FF9800',fillOpacity:0.8}).addTo(map)._tmpPoint=true;
|
||||||
|
showToast('📍 Точка '+(linePoints.length)+' линии. Enter — расставить опоры');
|
||||||
|
}
|
||||||
|
|
||||||
|
function finishLine(){
|
||||||
|
if(linePoints.length<2){showToast('Нарисуйте минимум 2 точки линии');return;}
|
||||||
|
const step=parseFloat(document.getElementById('poleStep').value)||50;
|
||||||
|
const margin=parseFloat(document.getElementById('entranceMargin').value)||5;
|
||||||
|
const totalLen=pathLength(linePoints);
|
||||||
|
const n=Math.max(1,Math.round(totalLen/step));
|
||||||
|
const poles=[];
|
||||||
|
for(let i=0;i<=n;i++){
|
||||||
|
const t=i/n;
|
||||||
|
const pt=pointOnPath(linePoints,t);
|
||||||
|
const distAlong=t*totalLen;
|
||||||
|
if(!isNearEntrance(pt,distAlong,margin)){
|
||||||
|
poles.push(pt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(!poles.length){showToast('❌ Все позиции перекрыты въездами');return;}
|
||||||
|
// clear temp drawings
|
||||||
|
map.eachLayer(l=>{if(l._tmpLine||l._tmpPoint)map.removeLayer(l);});
|
||||||
|
linePoints=[];
|
||||||
|
poles.forEach(p=>addPole(L.latLng(p.lat,p.lng)));
|
||||||
|
setMode('manual');
|
||||||
|
showToast('✅ Расставлено '+poles.length+' опор с шагом ~'+step+' м');
|
||||||
|
}
|
||||||
|
|
||||||
|
function pathLength(pts){
|
||||||
|
let total=0;
|
||||||
|
for(let i=1;i<pts.length;i++)total+=haversine(pts[i-1].lat,pts[i-1].lng,pts[i].lat,pts[i].lng);
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pointOnPath(pts,t){
|
||||||
|
const total=pathLength(pts);
|
||||||
|
let target=t*total,acc=0;
|
||||||
|
for(let i=1;i<pts.length;i++){
|
||||||
|
const seg=haversine(pts[i-1].lat,pts[i-1].lng,pts[i].lat,pts[i].lng);
|
||||||
|
if(acc+seg>=target||i===pts.length-1){
|
||||||
|
const frac=(target-acc)/seg;
|
||||||
|
return{lat:pts[i-1].lat+(pts[i].lat-pts[i-1].lat)*frac,lng:pts[i-1].lng+(pts[i].lng-pts[i-1].lng)*frac};
|
||||||
|
}
|
||||||
|
acc+=seg;
|
||||||
|
}
|
||||||
|
return pts[pts.length-1];
|
||||||
|
}
|
||||||
|
|
||||||
|
function isNearEntrance(pt,distAlong,margin){
|
||||||
|
for(const e of entrances){
|
||||||
|
const d=haversine(pt.lat,pt.lng,e.lat,e.lng);
|
||||||
|
if(d<margin)return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addEntrance(latlng){
|
||||||
|
const icon=L.divIcon({className:'',html:'<div class="entrance-label">🚗 В</div>',iconSize:[30,22],iconAnchor:[15,11]});
|
||||||
|
const marker=L.marker(latlng,{icon}).addTo(map).bindPopup('🚗 Въезд во двор');
|
||||||
|
entrances.push({lat:latlng.lat,lng:latlng.lng,marker});
|
||||||
|
const circle=L.circle(latlng,{radius:5,color:'#FF9800',fillColor:'#FF9800',fillOpacity:0.2,dashArray:'3,3'}).addTo(map);
|
||||||
|
entranceCircles.push(circle);
|
||||||
|
showToast('🚗 Въезд отмечен');
|
||||||
}
|
}
|
||||||
|
|
||||||
function haversine(lat1,lon1,lat2,lon2){
|
function haversine(lat1,lon1,lat2,lon2){
|
||||||
const R=6371000;
|
const R=6371000;
|
||||||
const toRad=x=>x*Math.PI/180;
|
const toRad=x=>x*Math.PI/180;
|
||||||
const dLat=toRad(lat2-lat1),dLon=toRad(lon2-lon1);
|
const dLat=toRad(lat2-lat1),dLon=toRad(lon2-lon1);
|
||||||
const a=Math.sin(dLat/2)**2+Math.cos(toRad(lat1))*Math.cos(toRad(lat2))*Math.sin(dLon/2)**2;
|
return R*2*Math.atan2(Math.sqrt(Math.sin(dLat/2)**2+Math.cos(toRad(lat1))*Math.cos(toRad(lat2))*Math.sin(dLon/2)**2),Math.sqrt(1-(Math.sin(dLat/2)**2+Math.cos(toRad(lat1))*Math.cos(toRad(lat2))*Math.sin(dLon/2)**2)));
|
||||||
return R*2*Math.atan2(Math.sqrt(a),Math.sqrt(1-a));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateTable(){
|
function updateTable(){
|
||||||
@ -111,60 +226,78 @@ function updateTable(){
|
|||||||
const dist=i>0?haversine(markers[i-1].lat,markers[i-1].lng,p.lat,p.lng):0;
|
const dist=i>0?haversine(markers[i-1].lat,markers[i-1].lng,p.lat,p.lng):0;
|
||||||
total+=dist;
|
total+=dist;
|
||||||
const tr=document.createElement('tr');
|
const tr=document.createElement('tr');
|
||||||
tr.innerHTML='<td class="num">'+p.id+'</td><td>'+p.name+'</td><td>'+p.lat.toFixed(6)+'</td><td>'+p.lng.toFixed(6)+'</td><td class="dist">'+(i>0?dist.toFixed(1)+' м':'—')+'</td>';
|
tr.innerHTML='<td class="num">'+p.id+'</td><td>'+p.name+'</td><td>'+p.lat.toFixed(6)+'</td><td>'+p.lng.toFixed(6)+'</td><td class="dist">'+(i>0?dist.toFixed(1):'—')+'</td>';
|
||||||
tbody.appendChild(tr);
|
tbody.appendChild(tr);
|
||||||
});
|
});
|
||||||
document.getElementById('totalDist').textContent='Итого: '+total.toFixed(1)+' м ('+markers.length+' опор)';
|
const avg=markers.length>1?(total/(markers.length-1)):0;
|
||||||
|
document.getElementById('totalDist').textContent='Итого: '+total.toFixed(1)+' м | '+markers.length+' опор | ср.пролёт '+avg.toFixed(1)+' м';
|
||||||
|
document.getElementById('stats').textContent=markers.length+' шт';
|
||||||
}
|
}
|
||||||
|
|
||||||
function undo(){
|
function undo(){
|
||||||
|
if(entrances.length){
|
||||||
|
const e=entrances.pop();
|
||||||
|
map.removeLayer(e.marker);
|
||||||
|
const c=entranceCircles.pop();
|
||||||
|
if(c)map.removeLayer(c);
|
||||||
|
showToast('↩ Въезд удалён');
|
||||||
|
return;
|
||||||
|
}
|
||||||
if(!markers.length)return;
|
if(!markers.length)return;
|
||||||
const last=markers.pop();
|
const last=markers.pop();
|
||||||
map.removeLayer(last.marker);
|
map.removeLayer(last.marker);
|
||||||
if(lines.length){map.removeLayer(lines.pop());}
|
if(lines.length){map.removeLayer(lines.pop());}
|
||||||
poleCount--;
|
poleCount--;
|
||||||
updateTable();
|
updateTable();
|
||||||
|
showToast('↩ Опора удалена');
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearAll(){
|
function clearAll(){
|
||||||
if(!markers.length)return;
|
if(!markers.length&&!entrances.length)return;
|
||||||
if(!confirm('Очистить все опоры?'))return;
|
if(!confirm('Удалить всё?'))return;
|
||||||
markers.forEach(m=>map.removeLayer(m.marker));
|
markers.forEach(m=>map.removeLayer(m.marker));
|
||||||
lines.forEach(l=>map.removeLayer(l));
|
lines.forEach(l=>map.removeLayer(l));
|
||||||
markers=[];lines=[];poleCount=0;
|
entrances.forEach(e=>map.removeLayer(e.marker));
|
||||||
|
entranceCircles.forEach(c=>map.removeLayer(c));
|
||||||
|
map.eachLayer(l=>{if(l._tmpLine||l._tmpPoint)map.removeLayer(l);});
|
||||||
|
markers=[];lines=[];entrances=[];entranceCircles=[];poleCount=0;linePoints=[];
|
||||||
updateTable();
|
updateTable();
|
||||||
|
showToast('🗑 Всё очищено');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.addEventListener('keydown',e=>{if(e.key==='Enter'&&mode==='line')finishLine();});
|
||||||
|
|
||||||
function showToast(msg){
|
function showToast(msg){
|
||||||
const t=document.getElementById('toast');
|
const t=document.getElementById('toast');
|
||||||
t.textContent=msg;t.classList.add('show');
|
t.textContent=msg;t.classList.add('show');
|
||||||
setTimeout(()=>t.classList.remove('show'),2000);
|
clearTimeout(t._timer);
|
||||||
|
t._timer=setTimeout(()=>t.classList.remove('show'),2500);
|
||||||
}
|
}
|
||||||
|
|
||||||
function exportCSV(){
|
function exportCSV(){
|
||||||
if(!markers.length){showToast('Нет опор для экспорта');return;}
|
if(!markers.length){showToast('Нет опор');return;}
|
||||||
let csv='№,Название,Широта,Долгота,Дистанция_м\n';
|
let csv='№,Название,Широта,Долгота,Расстояние_до_предыдущей_м\n';
|
||||||
markers.forEach((p,i)=>{
|
markers.forEach((p,i)=>{
|
||||||
const dist=i>0?haversine(markers[i-1].lat,markers[i-1].lng,p.lat,p.lng).toFixed(1):'';
|
const dist=i>0?haversine(markers[i-1].lat,markers[i-1].lng,p.lat,p.lng).toFixed(1):'';
|
||||||
csv+=p.id+','+p.name+','+p.lat.toFixed(6)+','+p.lng.toFixed(6)+','+dist+'\n';
|
csv+=p.id+','+p.name+','+p.lat.toFixed(6)+','+p.lng.toFixed(6)+','+dist+'\n';
|
||||||
});
|
});
|
||||||
download(csv,'poles.csv','text/csv');
|
download(csv,'opory_vl04.csv','text/csv;charset=utf-8');
|
||||||
}
|
}
|
||||||
|
|
||||||
function exportJSON(){
|
function exportJSON(){
|
||||||
if(!markers.length){showToast('Нет опор для экспорта');return;}
|
if(!markers.length){showToast('Нет опор');return;}
|
||||||
const data=markers.map((p,i)=>{
|
const data=markers.map((p,i)=>{
|
||||||
const dist=i>0?haversine(markers[i-1].lat,markers[i-1].lng,p.lat,p.lng):0;
|
const dist=i>0?haversine(markers[i-1].lat,markers[i-1].lng,p.lat,p.lng):0;
|
||||||
return{id:p.id,name:p.name,lat:p.lat,lng:p.lng,distance_m:parseFloat(dist.toFixed(1))};
|
return{id:p.id,name:p.name,lat:parseFloat(p.lat.toFixed(6)),lng:parseFloat(p.lng.toFixed(6)),distance_m:parseFloat(dist.toFixed(1))};
|
||||||
});
|
});
|
||||||
download(JSON.stringify(data,null,2),'poles.json','application/json');
|
download(JSON.stringify({poles:data,total_m:data.reduce((s,d)=>s+d.distance_m,0),average_span_m:data.length>1?(data.reduce((s,d)=>s+d.distance_m,0)/(data.length-1)):0},null,2),'opory_vl04.json','application/json');
|
||||||
}
|
}
|
||||||
|
|
||||||
function download(content,filename,type){
|
function download(content,filename,type){
|
||||||
const blob=new Blob([content],{type});
|
const blob=new Blob([content],{type:'application/octet-stream'});
|
||||||
const a=document.createElement('a');a.href=URL.createObjectURL(blob);
|
const a=document.createElement('a');a.href=URL.createObjectURL(blob);
|
||||||
a.download=filename;a.click();
|
a.download=filename;document.body.appendChild(a);a.click();document.body.removeChild(a);
|
||||||
showToast('Экспортировано: '+filename);
|
showToast('📥 '+filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
init();
|
init();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user