pole-planner/index.html

174 lines
8.2 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

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

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Планировщик опор</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
<style>
:root{--ink:#0F1218;--cyan:#00E5FF;--white:#fff;--gray-100:#F2F4F7;--gray-500:#5B6573}
*{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)}
.header{background:var(--ink);color:var(--white);padding:16px 24px;display:flex;align-items:center;gap:16px;flex-wrap:wrap}
.header h1{font-size:20px;font-weight:700}
.header .badge{background:var(--cyan);color:var(--ink);padding:4px 10px;border-radius:12px;font-size:12px;font-weight:700}
.controls{display:flex;gap:8px;flex-wrap:wrap}
.controls button{padding:8px 16px;border:none;border-radius:6px;font-weight:600;cursor:pointer;font-size:13px}
.btn-add{background:var(--cyan);color:var(--ink)}
.btn-undo{background:#ff5252;color:#fff}
.btn-export{background:#4caf50;color:#fff}
.btn-clear{background:#ff9800;color:#fff}
#map{height:calc(100vh - 200px);min-height:400px}
.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}
.sidebar-header{padding:12px 16px;background:var(--gray-100);font-weight:700;font-size:14px;border-bottom:1px solid #ddd}
.sidebar table{width:100%;border-collapse:collapse;font-size:13px}
.sidebar th{background:var(--ink);color:var(--white);padding:8px 10px;text-align:left}
.sidebar td{padding:6px 10px;border-bottom:1px solid var(--gray-100)}
.sidebar td.num{text-align:center;font-weight:700;color:var(--cyan)}
.sidebar td.dist{text-align:right;font-family:monospace}
.sidebar-footer{padding:10px 16px;background:var(--gray-100);font-size:13px;font-weight:600;border-top:1px solid #ddd}
.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)}
.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}
.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}
.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}}
</style>
</head>
<body>
<div class="header">
<h1>Планировщик опор</h1>
<span class="badge">Google Maps</span>
<div class="controls">
<button class="btn-add" id="btnAdd" onclick="toggleMode()">+ Добавить опору</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="exportJSON()">📥 JSON</button>
</div>
</div>
<div id="map"></div>
<div class="mode-badge" id="modeBadge" style="display:none">🎯 Кликните на карту, чтобы поставить опору</div>
<div class="sidebar">
<div class="sidebar-header">Таблица опор</div>
<div style="overflow-y:auto;flex:1">
<table>
<thead><tr><th></th><th>Название</th><th>Широта</th><th>Долгота</th><th>Дистанция</th></tr></thead>
<tbody id="poleTable"></tbody>
</table>
</div>
<div class="sidebar-footer" id="totalDist">Итого: 0 м (0 опор)</div>
</div>
<div class="toast" id="toast"></div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
let map, markers=[], lines=[], addMode=false, poleCount=0;
const LAYER_SATELLITE='https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}';
const LAYER_LABELS='https://server.arcgisonline.com/ArcGIS/rest/services/Reference/World_Boundaries_and_Places/MapServer/tile/{z}/{y}/{x}';
function init(){
map=L.map('map',{zoomControl:true}).setView([43.238,76.946],15);
L.tileLayer(LAYER_SATELLITE,{maxZoom:19,attribution:'Esri'}).addTo(map);
L.tileLayer(LAYER_LABELS,{maxZoom:19,opacity:0.7}).addTo(map);
map.on('click',onMapClick);
}
function toggleMode(){
addMode=!addMode;
document.getElementById('btnAdd').textContent=addMode?'✕ Отмена':'+ Добавить опору';
document.getElementById('modeBadge').style.display=addMode?'block':'none';
}
function onMapClick(e){
if(!addMode)return;
poleCount++;
const name='Опора '+poleCount;
const icon=L.divIcon({className:'',html:'<div class="pole-label">'+poleCount+'</div>',iconSize:[28,28],iconAnchor:[14,14]});
const marker=L.marker(e.latlng,{icon}).addTo(map).bindPopup('<b>'+name+'</b><br>'+e.latlng.lat.toFixed(6)+', '+e.latlng.lng.toFixed(6));
markers.push({id:poleCount,lat:e.latlng.lat,lng:e.latlng.lng,name,marker});
if(markers.length>1){
const prev=markers[markers.length-2];
const dist=haversine(prev.lat,prev.lng,e.latlng.lat,e.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);
lines.push(line);
}
updateTable();
showToast(name+' добавлена');
}
function haversine(lat1,lon1,lat2,lon2){
const R=6371000;
const toRad=x=>x*Math.PI/180;
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(a),Math.sqrt(1-a));
}
function updateTable(){
const tbody=document.getElementById('poleTable');
tbody.innerHTML='';
let total=0;
markers.forEach((p,i)=>{
const dist=i>0?haversine(markers[i-1].lat,markers[i-1].lng,p.lat,p.lng):0;
total+=dist;
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>';
tbody.appendChild(tr);
});
document.getElementById('totalDist').textContent='Итого: '+total.toFixed(1)+' м ('+markers.length+' опор)';
}
function undo(){
if(!markers.length)return;
const last=markers.pop();
map.removeLayer(last.marker);
if(lines.length){map.removeLayer(lines.pop());}
poleCount--;
updateTable();
}
function clearAll(){
if(!markers.length)return;
if(!confirm('Очистить все опоры?'))return;
markers.forEach(m=>map.removeLayer(m.marker));
lines.forEach(l=>map.removeLayer(l));
markers=[];lines=[];poleCount=0;
updateTable();
}
function showToast(msg){
const t=document.getElementById('toast');
t.textContent=msg;t.classList.add('show');
setTimeout(()=>t.classList.remove('show'),2000);
}
function exportCSV(){
if(!markers.length){showToast('Нет опор для экспорта');return;}
let csv='№,Название,Широта,Долгота,Дистанция_м\n';
markers.forEach((p,i)=>{
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';
});
download(csv,'poles.csv','text/csv');
}
function exportJSON(){
if(!markers.length){showToast('Нет опор для экспорта');return;}
const data=markers.map((p,i)=>{
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))};
});
download(JSON.stringify(data,null,2),'poles.json','application/json');
}
function download(content,filename,type){
const blob=new Blob([content],{type});
const a=document.createElement('a');a.href=URL.createObjectURL(blob);
a.download=filename;a.click();
showToast('Экспортировано: '+filename);
}
init();
</script>
</body>
</html>