diff --git a/server/static/app.js b/server/static/app.js index c528de7..377b7cd 100644 --- a/server/static/app.js +++ b/server/static/app.js @@ -2280,10 +2280,11 @@ function apiDebugReadForm() { txHeight: apiDebugNumber('apiDebugTxH', 30), rxHeight: apiDebugNumber('apiDebugRxH', 2), frequency: apiDebugNumber('apiDebugFreq', 433), - samples: Math.max(2, Math.min(2048, Math.round(apiDebugNumber('apiDebugSamples', 128)))), - model: document.getElementById('apiDebugModel')?.value || 'manual', - coverageModel: document.getElementById('apiDebugCoverageModel')?.value || 'itm', - includeCanopy: !!document.getElementById('apiDebugIncludeCanopy')?.checked + samples: Math.max(2, Math.min(2048, Math.round(apiDebugNumber('apiDebugSamples', 128)))), + model: document.getElementById('apiDebugModel')?.value || 'manual', + coverageModel: document.getElementById('apiDebugCoverageModel')?.value || 'itm', + coverageFormat: document.getElementById('apiDebugCoverageFormat')?.value || 'geojson', + includeCanopy: !!document.getElementById('apiDebugIncludeCanopy')?.checked }; } @@ -2431,6 +2432,25 @@ function renderApiDebugCoverageLayer(geojson, model) { return { rendered, levels }; } +function formatApiDebugCoverageRasterSummary(job, data) { + const meta = data?.metadata || {}; + const jobId = job?.jobId || '—'; + const format = data?.format || job?.result?.format || meta.format || '—'; + const artifactPath = `/api/v1/jobs/${encodeURIComponent(jobId)}/artifact`; + const artifactUrl = `${apiDebugReadForm().base}${artifactPath}`; + const stats = [ + `resolution ${fmtNum(meta.resolution_m, 0, ' m')}`, + `${meta.width ?? '—'}×${meta.height ?? '—'}`, + `valid ${meta.valid_pixels ?? '—'} px`, + `min ${fmtNum(meta.min_dbm, 1, ' dBm')}`, + `max ${fmtNum(meta.max_dbm, 1, ' dBm')}` + ].join(' · '); + return `Coverage raster: ${escapeHtml(format)} · model ${escapeHtml(job?.result?.model || '—')}
` + + `${stats}
` + + `Файл: ${escapeHtml(data?.uri || '—')}
` + + `Скачать через proxy: artifact`; +} + function renderApiDebugViewshedLayer(geojson) { return renderApiDebugGeoJsonLayer(geojson, { style: { @@ -2980,9 +3000,10 @@ function buildApiDebugRequest(kind) { azimuth_step_deg: 2, range_step_m: 50, include_buildings: true, + include_canopy: f.includeCanopy, include_vegetation: false, levels_dbm: [-90, -100, -110], - format: 'geojson' + format: f.coverageFormat } }; } @@ -3069,12 +3090,16 @@ async function runApiDebugDemo(kind) { return; } if (kind === 'coverage') { - const geojson = job.result?.data; + const coverageData = job.result?.data; const model = job.result?.model || apiDebugReadForm().coverageModel; - const { rendered, levels } = renderApiDebugCoverageLayer(geojson, model); - const features = geojson?.features?.length ?? 0; + if (coverageData?.kind === 'coverage_raster') { + apiDebugSetSummary(formatApiDebugCoverageRasterSummary(job, coverageData)); + return; + } + const { rendered, levels } = renderApiDebugCoverageLayer(coverageData, model); + const features = coverageData?.features?.length ?? 0; const fsplHint = model === 'fspl' - ? '
FSPL + omni = идеальный круг (рельеф не учитывается). Для формы по местности — ITM.' + ? '
FSPL + omni без surface obstruction даёт круг; сейчас API добавляет buildings/canopy loss, если включены соответствующие флаги.' : ''; if (rendered) { apiDebugSetSummary( @@ -3287,8 +3312,9 @@ function formatApiDebugBudgetSummary(budget) { return `Link budget (${escapeHtml(model)}): энергетически проходит ${fmtBool(budget.link_viable)} · margin ${fmtNum(budget.fade_margin_db, 1, ' dB')}
` + `Зона Френеля: ${escapeHtml(quality)} · clear ${fmtBool(budget.fresnel_clear)}
` + `RX power: ${fmtNum(budget.rx_power_dbm, 1, ' dBm')} · total loss ${fmtNum(budget.total_loss_db, 1, ' dB')}
` + - `FSPL: ${fmtNum(budget.fspl_db, 1, ' dB')} · diffraction ${fmtNum(budget.diffraction_db, 1, ' dB')} · ` + - `vegetation ${fmtNum(budget.vegetation_db, 1, ' dB')} · atmospheric ${fmtNum(budget.atmospheric_db, 1, ' dB')}` + + `Loss: FSPL ${fmtNum(budget.fspl_db, 1, ' dB')} · active excess ${fmtNum(budget.propagation_excess_db ?? budget.diffraction_db, 1, ' dB')} · ` + + `knife-edge ${fmtNum(budget.knife_edge_diffraction_db, 1, ' dB')}
` + + `Environment: vegetation ${fmtNum(budget.vegetation_db, 1, ' dB')} · atmospheric ${fmtNum(budget.atmospheric_db, 1, ' dB')}` + formatDataSourcesLine(budget.data_sources); } diff --git a/server/static/index.html b/server/static/index.html index dfc07e8..d16f6fa 100644 --- a/server/static/index.html +++ b/server/static/index.html @@ -50,7 +50,7 @@ - +
1. Выбор точек
@@ -80,7 +80,7 @@
3. Async jobs (Celery)
-
Viewshed и coverage ставятся в очередь; UI опрашивает /api/v1/jobs/{id} и подгружает GeoJSON артефакт. Coverage с FSPL даёт круг (без рельефа) — для реалистичного контура выберите ITM.
+
Viewshed и coverage ставятся в очередь; UI опрашивает /api/v1/jobs/{id}. Coverage умеет GeoJSON/PNG/GeoTIFF; canopy включайте только когда данные уже загружены.
+