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 @@
Расчёт линии
Высота TX
Только профиль
- Только LOS/Fresnel
+ Только LOS/Френель
Только link budget
Сектор антенны
@@ -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));
});