diff --git a/server/static/index.html b/server/static/index.html index 325a39d..dbcc8ee 100644 --- a/server/static/index.html +++ b/server/static/index.html @@ -294,7 +294,7 @@ - + @@ -311,11 +311,12 @@
“Расчёт линии” вызывает profile + LOS + link budget и строит график земли/поверхности.
- surface/препятствия + поверхность/препятствия луч TX→RX - границы 60% Fresnel - границы 100% Fresnel - нарушения + границы 60% зоны Френеля + границы 100% зоны Френеля + нарушения зоны Френеля +
Нет расчёта.
Выберите запрос для проверки RF API.
@@ -556,6 +557,7 @@ let apiDebugSurfaceProfile = null; let apiDebugLosLine = null; let apiDebugBaseSummaryHtml = 'Нет расчёта.'; + let apiDebugShowFresnel = true; let mapRulerMode = 'pick'; let mapRulerPtA = null; let mapRulerPtB = null; @@ -2015,6 +2017,27 @@ ctx.stroke(); }); } + if (s.segmentOverlay?.length) { + ctx.save(); + ctx.strokeStyle = s.segmentOverlayColor || '#ff1744'; + ctx.lineWidth = s.segmentOverlayWidth || 4; + ctx.lineCap = 'round'; + s.segmentOverlay.forEach(segment => { + if (!segment || segment.length < 2) return; + ctx.beginPath(); + segment.forEach((p, i) => { + const v = p.value ?? p[valueKey]; + const d = p.dist_m ?? p.distance_m; + if (v == null || d == null) return; + const x = margin.l + (d / Math.max(maxDist, 1)) * plotW; + const y = margin.t + plotH - ((v - minE) / (maxE - minE)) * plotH; + if (i === 0) ctx.moveTo(x, y); + else ctx.lineTo(x, y); + }); + ctx.stroke(); + }); + ctx.restore(); + } if (s.cursor != null && maxDist > 0) { const cx = margin.l + (s.cursor / maxDist) * plotW; const elev = profileValueAtDist(s.profile, s.cursor, valueKey); @@ -2304,7 +2327,7 @@ }); } if (apiDebugSurfaceProfile) { - if (apiDebugSurfaceProfile.fresnelMode) { + if (apiDebugSurfaceProfile.fresnelMode && apiDebugShowFresnel) { series.push({ color: '#ff9800', profile: apiDebugSurfaceProfile, @@ -2315,7 +2338,7 @@ series.push({ color: '#ffd54f', profile: apiDebugSurfaceProfile, - label: '60% Fresnel ниж.', + label: '60% зона Френеля ниж.', valueKey: 'fresnel_lower_60pct_m', dash: [6, 4], lineWidth: 1.5 @@ -2323,7 +2346,7 @@ series.push({ color: '#ffd54f', profile: apiDebugSurfaceProfile, - label: '60% Fresnel верх.', + label: '60% зона Френеля верх.', valueKey: 'fresnel_upper_60pct_m', dash: [6, 4], lineWidth: 1.5 @@ -2331,7 +2354,7 @@ series.push({ color: '#ba68c8', profile: apiDebugSurfaceProfile, - label: '100% Fresnel ниж.', + label: '100% зона Френеля ниж.', valueKey: 'fresnel_lower_100pct_m', dash: [3, 4], lineWidth: 1.3 @@ -2339,7 +2362,7 @@ series.push({ color: '#ba68c8', profile: apiDebugSurfaceProfile, - label: '100% Fresnel верх.', + label: '100% зона Френеля верх.', valueKey: 'fresnel_upper_100pct_m', dash: [3, 4], lineWidth: 1.3 @@ -2352,8 +2375,8 @@ valueKey: apiDebugSurfaceProfile.valueKey || 'elevation_m', losLine: apiDebugSurfaceProfile.fresnelMode ? null : apiDebugLosLine, cursor: apiDebugCursorDist, - pointsOverlay: apiDebugSurfaceProfile.violations || null, - pointsOverlayColor: '#ff1744' + segmentOverlay: apiDebugShowFresnel ? apiDebugSurfaceProfile.violationSegments : null, + segmentOverlayColor: '#ff1744' }); } renderElevationCanvas( @@ -2396,7 +2419,7 @@ `Точка на графике: ${fmtNum(apiDebugCursorDist, 0, ' m')} от TX
` + `Координаты: ${fmtNum(pt.lat, 6)}, ${fmtNum(pt.lon, 6)}
` + `Surface: ${fmtNum(surface, 1, ' m')} · луч: ${fmtNum(path, 1, ' m')}
` + - `60% Fresnel: ${fmtNum(fresnel60, 1, ' m')} .. ${fmtNum(fresnelUpper60, 1, ' m')} · ` + + `60% зона Френеля: ${fmtNum(fresnel60, 1, ' m')} .. ${fmtNum(fresnelUpper60, 1, ' m')} · ` + `100%: ${fmtNum(fresnel100, 1, ' m')} .. ${fmtNum(fresnelUpper100, 1, ' m')}
` + `Clearance над surface: ${fmtNum(clearance, 1, ' m')}` ); @@ -2685,15 +2708,26 @@ required_clearance_m: p.required_clearance_m != null ? Number(p.required_clearance_m) : null, type: p.type || null })).filter(p => Number.isFinite(p.dist_m) && p.surface_m != null); - const violations = points - .filter(p => p.clearance_m != null && p.required_clearance_m != null && p.clearance_m < p.required_clearance_m) - .map(p => ({ dist_m: p.dist_m, value: p.surface_m, type: p.type })); + const violationSegments = []; + let currentSegment = []; + points.forEach(p => { + const violated = p.clearance_m != null + && p.required_clearance_m != null + && p.clearance_m < p.required_clearance_m; + if (violated) { + currentSegment.push({ dist_m: p.dist_m, value: p.surface_m, type: p.type }); + } else if (currentSegment.length) { + if (currentSegment.length >= 2) violationSegments.push(currentSegment); + currentSegment = []; + } + }); + if (currentSegment.length >= 2) violationSegments.push(currentSegment); return { total_m: Number(points[points.length - 1]?.dist_m ?? 0), points, valueKey: 'surface_m', fresnelMode: true, - violations + violationSegments }; } @@ -2750,9 +2784,9 @@ function formatApiDebugLosSummary(los) { if (!los) return 'LOS: нет данных'; - return `LOS/Fresnel: ${fmtBool(los.los_clear)} · геометрия ${fmtBool(los.geometric_los)}
` + - `Fresnel clearance: ${fmtNum(los.first_fresnel_clearance_pct, 1, ' %')}
` + - `Препятствия: ${los.fresnel_violations_count ?? '—'} Fresnel, ` + + return `LOS/зона Френеля: ${fmtBool(los.los_clear)} · геометрия ${fmtBool(los.geometric_los)}
` + + `Просвет зоны Френеля: ${fmtNum(los.first_fresnel_clearance_pct, 1, ' %')}
` + + `Препятствия: ${los.fresnel_violations_count ?? '—'} зона Френеля, ` + `${los.geometric_obstructions_count ?? '—'} геометрия, ${los.building_obstructions_count ?? '—'} здания
` + `Потери: diffraction ${fmtNum(los.diffraction_loss_db, 1, ' dB')}, ` + `vegetation ${fmtNum(los.vegetation_loss_db, 1, ' dB')}`; @@ -3734,6 +3768,10 @@ ['apiDebugTxLat', 'apiDebugTxLon', 'apiDebugRxLat', 'apiDebugRxLon'].forEach(id => { document.getElementById(id)?.addEventListener('change', refreshApiDebugOverlay); }); + document.getElementById('apiDebugShowFresnel')?.addEventListener('change', e => { + apiDebugShowFresnel = e.target.checked; + drawApiDebugChart(); + }); document.querySelectorAll('[data-api-demo]').forEach(btn => { btn.addEventListener('click', () => runApiDebugDemo(btn.dataset.apiDemo)); });