diff --git a/server/static/app.js b/server/static/app.js
index 1c96fc3..03f5a0e 100644
--- a/server/static/app.js
+++ b/server/static/app.js
@@ -1873,6 +1873,91 @@ function renderApiDebugViewshedLayer(geojson) {
});
}
+function fitApiDebugLayer(layer) {
+ const bounds = layer?.getBounds?.();
+ if (bounds?.isValid?.()) {
+ setMapViewProgrammatically(() => map.fitBounds(bounds, { padding: [40, 40], maxZoom: 16 }));
+ }
+}
+
+function renderApiDebugFresnelSlice(data) {
+ clearApiDebugCoverage();
+ clearApiDebugBeam();
+ const layers = [];
+ const pathLayer = data?.path_geojson ? L.geoJSON(data.path_geojson, {
+ style: { color: '#4fc3f7', weight: 3, opacity: 0.95 }
+ }) : null;
+ const fresnelLayer = data?.fresnel_geojson ? L.geoJSON(data.fresnel_geojson, {
+ style: {
+ color: '#ffd54f',
+ weight: 2,
+ opacity: 0.95,
+ fillColor: '#ffd54f',
+ fillOpacity: 0.2
+ }
+ }) : null;
+ const buildingsLayer = data?.buildings_geojson ? L.geoJSON(data.buildings_geojson, {
+ style: {
+ color: '#ff1744',
+ weight: 1.5,
+ opacity: 0.95,
+ fillColor: '#ff1744',
+ fillOpacity: 0.3
+ },
+ onEachFeature: (feature, layer) => {
+ const p = feature.properties || {};
+ const parts = [];
+ if (p.height_m != null) parts.push(`height ${fmtNum(p.height_m, 1, ' m')}`);
+ if (p.intersection_area_m2 != null) parts.push(`пересечение ${fmtNum(p.intersection_area_m2, 1, ' m²')}`);
+ if (p.building_type) parts.push(escapeHtml(p.building_type));
+ if (parts.length) layer.bindPopup(parts.join(' · '));
+ }
+ }) : null;
+ [fresnelLayer, buildingsLayer, pathLayer].forEach(layer => {
+ if (layer) layers.push(layer);
+ });
+ if (!layers.length) {
+ apiDebugSetSummary('Ответ fresnel-slice не содержит GeoJSON слоёв.');
+ return;
+ }
+ apiDebugCoverageLayer = L.featureGroup(layers).addTo(map);
+ fitApiDebugLayer(apiDebugCoverageLayer);
+ const buildings = data?.buildings_geojson?.features?.length ?? 0;
+ const profilePoints = data?.profile?.length ?? 0;
+ apiDebugSetSummary(
+ `Срез зоны Френеля: ${profilePoints} точек профиля · ${buildings} зданий пересекают область
` +
+ `Жёлтая область — горизонтальный срез 60% зоны Френеля, красные полигоны — пересекающие здания.` +
+ formatDataSourcesLine(data?.data_sources)
+ );
+}
+
+function renderApiDebugBuildings(data) {
+ const features = data?.features?.length ?? 0;
+ const rendered = renderApiDebugGeoJsonLayer(data, {
+ style: {
+ color: '#ff1744',
+ weight: 1.3,
+ opacity: 0.95,
+ fillColor: '#ff1744',
+ fillOpacity: 0.22
+ },
+ onEachFeature: (feature, layer) => {
+ const p = feature.properties || {};
+ const parts = [];
+ if (p.height_m != null) parts.push(`height ${fmtNum(p.height_m, 1, ' m')}`);
+ if (p.levels != null) parts.push(`${p.levels} эт.`);
+ if (p.building_type) parts.push(escapeHtml(p.building_type));
+ if (p.source) parts.push(escapeHtml(p.source));
+ if (parts.length) layer.bindPopup(parts.join(' · '));
+ }
+ });
+ apiDebugSetSummary(
+ rendered
+ ? `Здания вдоль линии: ${features} объектов в буфере 50 m${formatDataSourcesLine(data?.data_sources)}`
+ : 'Ответ buildings/query не содержит GeoJSON features.'
+ );
+}
+
async function fetchApiDebugJobArtifact(jobId) {
const f = apiDebugReadForm();
const url = `${f.base}/api/v1/jobs/${encodeURIComponent(jobId)}/artifact`;
@@ -2221,6 +2306,23 @@ function buildApiDebugRequest(kind) {
}
};
}
+ if (kind === 'fresnelSlice') {
+ return {
+ method: 'POST',
+ path: '/api/v1/terrain/fresnel-slice',
+ body: {
+ tx,
+ rx,
+ frequency_mhz: f.frequency,
+ fresnel_fraction: 0.6,
+ include_buildings: true,
+ include_canopy: f.includeCanopy,
+ k_factor: 1.333,
+ samples: f.samples,
+ building_limit: 5000
+ }
+ };
+ }
if (kind === 'budget') {
return {
method: 'POST',
@@ -2237,6 +2339,22 @@ function buildApiDebugRequest(kind) {
}
};
}
+ if (kind === 'buildings') {
+ return {
+ method: 'POST',
+ path: '/api/v1/buildings/query',
+ body: {
+ path: {
+ type: 'LineString',
+ coordinates: [
+ [f.tx.lon, f.tx.lat],
+ [f.rx.lon, f.rx.lat]
+ ]
+ },
+ buffer_m: 50
+ }
+ };
+ }
if (kind === 'landcover') {
return {
method: 'POST',
@@ -2429,7 +2547,9 @@ async function runApiDebugDemo(kind) {
}
if (kind === 'profile') renderApiDebugProfile(data, null, null);
else if (kind === 'los') renderApiDebugProfile(null, data, null);
+ else if (kind === 'fresnelSlice') renderApiDebugFresnelSlice(data);
else if (kind === 'budget') apiDebugSetSummary(formatApiDebugBudgetSummary(data));
+ else if (kind === 'buildings') renderApiDebugBuildings(data);
else if (kind === 'landcover') apiDebugSetSummary(formatApiDebugLandcoverSummary(data));
else if (kind === 'elevation') apiDebugSetSummary(formatApiDebugPointSummary(data));
else if (kind === 'beam') renderApiDebugBeam(data);
diff --git a/server/static/index.html b/server/static/index.html
index d313763..5a99363 100644
--- a/server/static/index.html
+++ b/server/static/index.html
@@ -71,7 +71,9 @@
+
+