From fd61ca072684f4b92f6a4de439171b466cd16f64 Mon Sep 17 00:00:00 2001 From: grigo Date: Wed, 24 Jun 2026 08:07:41 +0300 Subject: [PATCH] updated api --- server/static/app.js | 117 +++++++++++++++++++++++++++++++++++---- server/static/index.html | 11 +++- 2 files changed, 115 insertions(+), 13 deletions(-) diff --git a/server/static/app.js b/server/static/app.js index fd21922..1c96fc3 100644 --- a/server/static/app.js +++ b/server/static/app.js @@ -1705,9 +1705,10 @@ 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', - 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', + includeCanopy: !!document.getElementById('apiDebugIncludeCanopy')?.checked }; } @@ -1800,14 +1801,18 @@ function renderApiDebugGeoJsonLayer(geojson, options = {}) { const feature = geojson?.type === 'FeatureCollection' ? geojson : (geojson?.geojson || geojson); if (!feature?.features && !feature?.geometry) return false; clearApiDebugCoverage(); - apiDebugCoverageLayer = L.geoJSON(feature, { - style: options.style || { + const styleFn = typeof options.style === 'function' + ? options.style + : () => (options.style || { color: '#4fc3f7', weight: 2, opacity: 0.9, fillColor: '#4fc3f7', fillOpacity: 0.12 - } + }); + apiDebugCoverageLayer = L.geoJSON(feature, { + style: styleFn, + onEachFeature: options.onEachFeature }).addTo(map); const bounds = apiDebugCoverageLayer.getBounds(); if (bounds.isValid()) { @@ -1816,6 +1821,74 @@ function renderApiDebugGeoJsonLayer(geojson, options = {}) { return true; } +const API_DEBUG_COVERAGE_COLORS = { + '-90': '#4fc3f7', + '-100': '#29b6f6', + '-110': '#0288d1', + '-120': '#01579b' +}; + +function coverageLevelStyle(feature) { + const level = feature?.properties?.level_dbm; + const color = API_DEBUG_COVERAGE_COLORS[String(level)] || '#4fc3f7'; + return { + color, + weight: 2, + opacity: 0.95, + fillColor: color, + fillOpacity: 0.14 + }; +} + +function renderApiDebugCoverageLayer(geojson, model) { + const levels = [...new Set((geojson?.features || []).map(f => f.properties?.level_dbm))].sort((a, b) => b - a); + const rendered = renderApiDebugGeoJsonLayer(geojson, { + style: coverageLevelStyle, + onEachFeature: (feature, layer) => { + const p = feature.properties || {}; + const parts = []; + if (p.level_dbm != null) parts.push(`${p.level_dbm} dBm`); + if (p.model || model) parts.push(p.model || model); + if (p.frequency_mhz != null) parts.push(`${p.frequency_mhz} MHz`); + if (parts.length) layer.bindPopup(parts.join(' · ')); + } + }); + return { rendered, levels }; +} + +function renderApiDebugViewshedLayer(geojson) { + return renderApiDebugGeoJsonLayer(geojson, { + style: { + color: '#00ff88', + weight: 2, + opacity: 0.95, + fillColor: '#00ff88', + fillOpacity: 0.22 + }, + onEachFeature: (feature, layer) => { + if (feature.properties?.visible != null) { + layer.bindPopup('Видимая зона (viewshed)'); + } + } + }); +} + +async function fetchApiDebugJobArtifact(jobId) { + const f = apiDebugReadForm(); + const url = `${f.base}/api/v1/jobs/${encodeURIComponent(jobId)}/artifact`; + apiDebugSetStatus(`GET ${url}`); + const res = await fetch(url, { cache: 'no-store' }); + const text = await res.text(); + let data = text; + try { data = text ? JSON.parse(text) : null; } catch (e) {} + if (!res.ok) { + const err = new Error(`HTTP ${res.status}`); + err.response = data || res.statusText; + throw err; + } + return data; +} + function renderApiDebugBeam(data) { const feature = data?.geojson || data; if (!feature?.geometry) { @@ -2208,7 +2281,7 @@ function buildApiDebugRequest(kind) { gain_dbi: 8 }, rx: { height_agl: f.rxHeight, sensitivity_dbm: -110, gain_dbi: 2 }, - model: 'fspl', + model: f.coverageModel, environment: 'urban', radius_m: 12000, azimuth_step_deg: 2, @@ -2304,22 +2377,42 @@ async function runApiDebugDemo(kind) { } if (kind === 'coverage') { const geojson = job.result?.data; + const model = job.result?.model || apiDebugReadForm().coverageModel; + const { rendered, levels } = renderApiDebugCoverageLayer(geojson, model); const features = geojson?.features?.length ?? 0; - if (renderApiDebugGeoJsonLayer(geojson)) { + const fsplHint = model === 'fspl' + ? '
FSPL + omni = идеальный круг (рельеф не учитывается). Для формы по местности — ITM.' + : ''; + if (rendered) { apiDebugSetSummary( - `Coverage job: ${escapeHtml(jobId)} · model ${escapeHtml(job.result?.model || '—')} · ` + - `${features} контуров на карте (голубая заливка).` + `Coverage job: ${escapeHtml(jobId)} · model ${escapeHtml(model)} · ` + + `${features} контур(ов): ${levels.map(l => `${l} dBm`).join(', ') || '—'}${fsplHint}` ); } else { apiDebugSetSummary(`Coverage job ${escapeHtml(jobId)} завершён, GeoJSON не получен.`); } } else { const meta = job.result?.metadata || {}; + let viewshedRendered = false; + if (meta.format === 'geojson' && job.result?.uri) { + try { + apiDebugSetStatus(`Job ${jobId}: загрузка GeoJSON…`); + const geojson = await fetchApiDebugJobArtifact(jobId); + apiDebugSetOutput({ job, artifact: geojson }); + viewshedRendered = renderApiDebugViewshedLayer(geojson); + } catch (e) { + apiDebugSetOutput({ job, artifact_error: e.response || String(e) }); + } + } apiDebugSetSummary( `Viewshed job: ${escapeHtml(jobId)} · ` + `format ${escapeHtml(meta.format || '—')} · radius ${fmtNum(meta.radius_m, 0, ' m')} · ` + - `features ${meta.feature_count ?? '—'}
` + - `Файл: ${escapeHtml(job.result?.uri || '—')}` + `features ${meta.feature_count ?? '—'}` + + (viewshedRendered ? '
Зелёная заливка на карте — видимая зона.' : '') + + `
Файл: ${escapeHtml(job.result?.uri || '—')}` + + (!viewshedRendered && meta.format === 'geojson' + ? '
GeoJSON на сервере — нужен endpoint /jobs/{id}/artifact (обновите RadioApi).' + : '') ); } return; diff --git a/server/static/index.html b/server/static/index.html index b1cb397..d313763 100644 --- a/server/static/index.html +++ b/server/static/index.html @@ -78,7 +78,16 @@
3. Async jobs (Celery)
-
Viewshed и coverage ставятся в очередь; UI опрашивает /api/v1/jobs/{id} до статуса done/error.
+
Viewshed и coverage ставятся в очередь; UI опрашивает /api/v1/jobs/{id} и подгружает GeoJSON артефакт. Coverage с FSPL даёт круг (без рельефа) — для реалистичного контура выберите ITM.
+
+ +