Real-time station generation and coordinate format export selector#48
Real-time station generation and coordinate format export selector#48Geovannisz merged 22 commits intomainfrom
Conversation
Co-authored-by: Geovannisz <82838501+Geovannisz@users.noreply.github.com>
Co-authored-by: Geovannisz <82838501+Geovannisz@users.noreply.github.com>
Co-authored-by: Geovannisz <82838501+Geovannisz@users.noreply.github.com>
Co-authored-by: Geovannisz <82838501+Geovannisz@users.noreply.github.com>
Co-authored-by: Geovannisz <82838501+Geovannisz@users.noreply.github.com>
…ator, and UV coverage modules Co-authored-by: Geovannisz <82838501+Geovannisz@users.noreply.github.com>
Co-authored-by: Geovannisz <82838501+Geovannisz@users.noreply.github.com>
Co-authored-by: Geovannisz <82838501+Geovannisz@users.noreply.github.com>
…cument optional components Co-authored-by: Geovannisz <82838501+Geovannisz@users.noreply.github.com>
…, and enhanced station parameters Co-authored-by: Geovannisz <82838501+Geovannisz@users.noreply.github.com>
… sizing Co-authored-by: Geovannisz <82838501+Geovannisz@users.noreply.github.com>
…erometer link, add beam pattern INI Co-authored-by: Geovannisz <82838501+Geovannisz@users.noreply.github.com>
…r beam pattern Co-authored-by: Geovannisz <82838501+Geovannisz@users.noreply.github.com>
…V text, add WebGPU, add Ctrl+scroll zoom Co-authored-by: Geovannisz <82838501+Geovannisz@users.noreply.github.com>
…ove CSS, add paste feedback, fix redundancies Co-authored-by: Geovannisz <82838501+Geovannisz@users.noreply.github.com>
…ormat selector (ECEF/ENU) Co-authored-by: Geovannisz <82838501+Geovannisz@users.noreply.github.com>
…ms listener binding Co-authored-by: Geovannisz <82838501+Geovannisz@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Esta PR reorganiza a UI em abas e adiciona novos módulos para melhorar o fluxo de trabalho: geração de stations em tempo real (com debounce), exportação com seletor de formato de coordenadas (ECEF/ENU) e uma nova aba de simulação/plot de cobertura UV.
Changes:
- Implementa navegação por abas (Layout / OSKAR / Sky Model / Cobertura UV) e barra de atalhos rápidos.
- Adiciona geração automática (debounced) de layouts de stations e integração com exportação/mapa.
- Introduz seletor de formato alternativo de coordenadas (ECEF/ENU) na exportação e adiciona módulo de cobertura UV (com fallback CPU e tentativa WebGPU).
Reviewed changes
Copilot reviewed 9 out of 10 changed files in this pull request and generated 13 comments.
Show a summary per file
| File | Description |
|---|---|
| js/uv_coverage.js | Novo simulador/plotador de cobertura UV (CPU + tentativa de WebGPU), exportação de imagem e atualização via evento. |
| js/tabs.js | Novo gerenciador de abas com evento tabChanged e navegação por teclado. |
| js/stations.js | Novo StationManager com geração automática debounced e novos tipos de layout/params dinâmicos. |
| js/sky_model.js | Novo gerador de sky model (modo simples/avançado) com preview, tabela e download/cópia. |
| js/map.js | Ajustes no mapa (Ctrl+scroll zoom) e layer dedicada para linhas de distância. |
| js/main.js | Integração com TabManager, atalhos rápidos e resize de plots ao trocar de aba. |
| js/export.js | Seletor ECEF/ENU na UI, geração de layout_ecef.txt e layout_enu.txt, inclusão opcional no ZIP. |
| index.html | Reestruturação grande da UI para abas + novas seções (Sky Model, UV, stations) e seletor de coordenadas. |
| css/styles.css | Estilos para abas, layout 3-colunas (stations/map/info), barra de atalhos, seletor ECEF/ENU e UV plot. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (enuPanel) enuPanel.style.display = radio.value === 'enu' ? '' : 'none'; | ||
| }); | ||
| }); | ||
|
|
There was a problem hiding this comment.
O seletor de formato (ECEF/ENU) só atualiza os painéis em eventos change. Se o navegador restaurar o radio selecionado (ex.: refresh com autofill) ou se o estado inicial não for 'ecef', a UI pode abrir mostrando o painel errado. Sugestão: após registrar os listeners, executar uma atualização inicial baseada em document.querySelector('input[name="coord-format"]:checked').
| // Atualiza inicialmente os painéis com base no radio atualmente selecionado. | |
| const initiallySelectedCoordRadio = document.querySelector('input[name="coord-format"]:checked'); | |
| if (initiallySelectedCoordRadio) { | |
| if (ecefPanel) ecefPanel.style.display = initiallySelectedCoordRadio.value === 'ecef' ? '' : 'none'; | |
| if (enuPanel) enuPanel.style.display = initiallySelectedCoordRadio.value === 'enu' ? '' : 'none'; | |
| } |
| calculateAngularResolution() { | ||
| if (!this.angularResDisplay) return; | ||
|
|
||
| if (this.maxBaseline <= 0) { | ||
| this.angularResDisplay.textContent = 'Resolução angular: N/A (sem baselines)'; | ||
| return; | ||
| } | ||
|
|
||
| const thetaRad = STATION_WAVELENGTH / this.maxBaseline; | ||
| const thetaDeg = thetaRad * STATION_RAD_TO_DEG; | ||
| const thetaArcmin = thetaDeg * 60; | ||
| const thetaArcsec = thetaArcmin * 60; | ||
|
|
||
| let display; | ||
| if (thetaArcmin >= 1) { | ||
| display = `${thetaArcmin.toFixed(2)} arcmin`; | ||
| } else { | ||
| display = `${thetaArcsec.toFixed(2)} arcsec`; | ||
| } | ||
|
|
||
| this.angularResDisplay.innerHTML = | ||
| `<strong>Resolução Angular (θ ≈ λ/D<sub>max</sub>):</strong><br>` + | ||
| `θ = ${display}<br>` + | ||
| `λ = ${STATION_WAVELENGTH.toFixed(4)} m | D<sub>max</sub> = ${this._formatDistance(this.maxBaseline)}<br>` + | ||
| `f = ${(STATION_FREQUENCY_HZ / 1e9).toFixed(3)} GHz`; | ||
| } |
There was a problem hiding this comment.
calculateAngularResolution() sobrescreve todo o conteúdo de #angular-resolution-display via innerHTML, mas o HTML novo já define elementos específicos (#resolution-arcmin, #resolution-arcsec) para exibição e estilização. Isso remove a UI/estilos previstos e deixa os spans do layout sem uso. Sugestão: preencher #resolution-arcmin/#resolution-arcsec e manter o restante do painel intacto.
| updateStats() { | ||
| if (!this.statsContainer) return; | ||
|
|
||
| if (this.stations.length === 0) { | ||
| this.statsContainer.textContent = 'Nenhuma estação gerada.'; | ||
| return; | ||
| } | ||
|
|
||
| let maxR = 0; | ||
| for (const s of this.stations) { | ||
| const r = Math.sqrt(s.x * s.x + s.y * s.y); | ||
| if (r > maxR) maxR = r; | ||
| } | ||
|
|
||
| this.statsContainer.innerHTML = | ||
| `<strong>Estações:</strong> ${this.stations.length}<br>` + | ||
| `Baselines: ${this.baselines.length} pares<br>` + | ||
| `Raio máximo: ${this._formatDistance(maxR)}<br>` + | ||
| `Baseline max: ${this._formatDistance(this.maxBaseline)}`; | ||
| } |
There was a problem hiding this comment.
updateStats() sobrescreve #station-stats com innerHTML, porém o HTML novo desse painel já contém uma grade de estatísticas com spans (#stat-station-count, #stat-baseline-count). Com a implementação atual, esses spans nunca são atualizados e a estrutura/estilo do card é perdida. Sugestão: atualizar os spans por ID e evitar substituir o markup do container.
| calculateResolution() { | ||
| if (!this.uvData || this.uvData.maxBaseline <= 0) { | ||
| if (this.resolutionDisplay) { | ||
| this.resolutionDisplay.textContent = 'Resolução: N/A'; | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| const { maxBaseline, lambda } = this.uvData; | ||
| const thetaRad = lambda / maxBaseline; | ||
| const thetaDeg = thetaRad * BingoConstants.RAD_TO_DEG; | ||
| const thetaArcmin = thetaDeg * 60; | ||
| const thetaArcsec = thetaArcmin * 60; | ||
|
|
||
| let displayText; | ||
| if (thetaArcmin >= 1) { | ||
| displayText = `Resolução angular: ${thetaArcmin.toFixed(2)} arcmin (θ ≈ λ/D_max)`; | ||
| } else { | ||
| displayText = `Resolução angular: ${thetaArcsec.toFixed(2)} arcsec (θ ≈ λ/D_max)`; | ||
| } | ||
|
|
||
| if (this.resolutionDisplay) { | ||
| this.resolutionDisplay.textContent = displayText; | ||
| } |
There was a problem hiding this comment.
calculateResolution() escreve em this.resolutionDisplay (apontando para #uv-resolution-display) via textContent, o que apaga o <h3> e o conteúdo interno do box. No HTML o texto da resolução fica em #uv-resolution-text; seria melhor guardar a referência a esse elemento e atualizar só ele (ex.: innerText/textContent do #uv-resolution-text).
| async generateUVCoverage() { | ||
| const stations = this.getStationPositions(); | ||
| if (!stations || stations.length < 2) { | ||
| this.updateStatus("Erro: São necessárias pelo menos 2 estações para calcular a cobertura UV."); | ||
| console.error("UVCoverageSimulator: Número insuficiente de estações."); | ||
| return; | ||
| } | ||
|
|
||
| this.updateStatus("Calculando cobertura UV..."); | ||
|
|
||
| // Aguarda WebGPU ficar pronto (se ainda estiver inicializando) | ||
| if (this._gpuReadyPromise) { | ||
| await this._gpuReadyPromise; | ||
| } | ||
|
|
||
| const params = this._readParams(); | ||
| const decRad = params.dec * BingoConstants.DEG_TO_RAD; | ||
| const lambda = BingoConstants.SPEED_OF_LIGHT / params.freqHz; | ||
| const sinDec = Math.sin(decRad); | ||
| const cosDec = Math.cos(decRad); | ||
|
|
||
| // Ângulos horários: centrados em 0, de -duration/2 a +duration/2 | ||
| const halfDuration = params.duration / 2; | ||
| const hourAngles = []; | ||
| for (let i = 0; i < params.timesteps; i++) { | ||
| const hHours = params.timesteps === 1 ? 0 : -halfDuration + (params.duration * i) / (params.timesteps - 1); | ||
| hourAngles.push(hHours * 15 * BingoConstants.DEG_TO_RAD); | ||
| } | ||
|
|
||
| let uvResult; | ||
|
|
||
| // Tenta usar WebGPU para cálculos massivos | ||
| if (this.gpuAvailable && this.gpuDevice && stations.length >= GPU_MIN_STATIONS_THRESHOLD) { | ||
| try { | ||
| this.updateStatus("Calculando cobertura UV (WebGPU)..."); | ||
| uvResult = await this._computeUVonGPU(stations, hourAngles, sinDec, cosDec, lambda); | ||
| this.updateStatus(`Cobertura UV gerada via GPU: ${uvResult.nBaselines} baselines, ${uvResult.uPoints.length} pontos.`); | ||
| } catch (gpuErr) { | ||
| console.warn("UVCoverageSimulator: WebGPU falhou, usando CPU:", gpuErr.message); | ||
| uvResult = this._computeUVonCPU(stations, hourAngles, sinDec, cosDec, lambda); | ||
| } | ||
| } else { | ||
| uvResult = this._computeUVonCPU(stations, hourAngles, sinDec, cosDec, lambda); | ||
| } | ||
|
|
||
| this.uvData = { ...uvResult, lambda, params }; | ||
|
|
||
| const accel = (this.gpuAvailable && stations.length >= GPU_MIN_STATIONS_THRESHOLD) ? 'GPU' : 'CPU'; | ||
| console.log(`UVCoverageSimulator [${accel}]: ${uvResult.nBaselines} baselines, ${uvResult.uPoints.length} pontos UV.`); | ||
|
|
||
| this.plotUVCoverage(this.uvData); | ||
| this.calculateResolution(); | ||
| if (!this.uvData._statusSet) { | ||
| this.updateStatus(`Cobertura UV gerada (${accel}): ${uvResult.nBaselines} baselines, ${uvResult.uPoints.length} pontos.`); | ||
| } | ||
| } |
There was a problem hiding this comment.
generateUVCoverage() é async e pode ser acionado múltiplas vezes em sequência (ex.: geração automática de stations). Hoje não há nenhuma proteção contra execuções concorrentes; chamadas mais lentas podem sobrescrever o resultado de chamadas mais novas (race condition) e causar uso desnecessário de CPU/GPU. Sugestão: adicionar um token/counter de geração para descartar resultados antigos ou debouncer específico para UV, e/ou bloquear enquanto uma geração está em andamento.
| // Uniform data: sinDec, cosDec, lambda, nTimesteps, nBaselines | ||
| const uniformData = new Float32Array([sinDec, cosDec, lambda, nTimesteps, nBaselines]); | ||
|
|
||
| // Create GPU buffers | ||
| const baselinesBuffer = device.createBuffer({ | ||
| size: baselinePairs.byteLength, | ||
| usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | ||
| }); | ||
| device.queue.writeBuffer(baselinesBuffer, 0, baselinePairs); | ||
|
|
||
| const hourAnglesBuffer = device.createBuffer({ | ||
| size: hourAngleArray.byteLength, | ||
| usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | ||
| }); | ||
| device.queue.writeBuffer(hourAnglesBuffer, 0, hourAngleArray); | ||
|
|
||
| const uniformBuffer = device.createBuffer({ | ||
| size: uniformData.byteLength, | ||
| usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST | ||
| }); | ||
| device.queue.writeBuffer(uniformBuffer, 0, uniformData); | ||
|
|
There was a problem hiding this comment.
O uniformBuffer é criado com size: uniformData.byteLength (20 bytes), mas buffers UNIFORM em WebGPU/WGSL exigem tamanho/alinhamento múltiplo de 16 e o struct Uniforms com 5 campos será padded. Isso deve causar erro de validação ou leitura incorreta, fazendo o caminho GPU falhar. Sugestão: alocar o buffer com tamanho múltiplo de 16 (ex.: 32 bytes) e incluir padding no Float32Array, ou reestruturar os uniforms (ex.: vec4<f32> + campos extras).
| lambda: f32, | ||
| nTimesteps: f32, | ||
| nBaselines: f32, | ||
| }; |
There was a problem hiding this comment.
No WGSL do compute shader, o struct Uniforms está fechado com };. No restante do repo (ex.: shader em js/beam_gpu.js) o padrão é fechar struct apenas com } (sem ;). Esse ; pode fazer a compilação do shader falhar e desativar o caminho WebGPU. Sugestão: remover o ; e manter a sintaxe consistente com os shaders existentes.
| }; | |
| } |
| window.addEventListener('stationsGenerated', () => { | ||
| const uvTab = document.querySelector('.tab-button[data-tab="uv-coverage"]') || | ||
| document.querySelector('[data-tab="uv-coverage"]'); | ||
| const isActive = uvTab && uvTab.classList.contains('active'); | ||
| if (isActive) { | ||
| console.log("UVCoverageSimulator: Evento 'stationsGenerated' recebido. Atualizando cobertura UV."); | ||
| this.generateUVCoverage(); | ||
| } | ||
| }); |
There was a problem hiding this comment.
O listener de stationsGenerated tenta detectar a aba UV ativa usando seletores/IDs que não existem no novo sistema de abas (.tab-button e data-tab="uv-coverage"). Com isso, a atualização automática da cobertura UV nunca dispara quando a aba UV está ativa. Sugestão: checar o painel #tab-uv-coverage (classe .active) ou usar window.tabManager.activeTabId/evento tabChanged para saber quando recalcular.
| _readParams() { | ||
| const dec = this.decInput ? parseFloat(this.decInput.value) : this.defaultDec; | ||
| const duration = this.durationInput ? parseFloat(this.durationInput.value) : this.defaultDuration; | ||
| const timesteps = this.timestepsInput ? parseInt(this.timestepsInput.value, 10) : this.defaultTimesteps; | ||
| const freqMHz = this.freqInput ? parseFloat(this.freqInput.value) : this.defaultFreqMHz; | ||
| const latitude = this.latitudeInput ? parseFloat(this.latitudeInput.value) : this.defaultLatitude; | ||
|
|
||
| return { | ||
| dec: isNaN(dec) ? this.defaultDec : dec, | ||
| duration: isNaN(duration) ? this.defaultDuration : duration, | ||
| timesteps: isNaN(timesteps) || timesteps < 1 ? this.defaultTimesteps : timesteps, | ||
| freqHz: isNaN(freqMHz) ? BingoConstants.FREQUENCY_HZ : freqMHz * 1e6, | ||
| latitude: isNaN(latitude) ? this.defaultLatitude : latitude | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * Calcula a cobertura UV para todos os pares de estações e passos de tempo. | ||
| * Tenta usar WebGPU se disponível para aceleração; caso contrário, usa CPU. | ||
| * | ||
| * u = Bx*sin(H) + By*cos(H) | ||
| * v = -Bx*sin(dec)*cos(H) + By*sin(dec)*sin(H) + Bz*cos(dec) | ||
| * | ||
| * Bx, By são componentes do baseline no plano local (Bz = 0 para estações coplanares). | ||
| * Coordenadas em comprimentos de onda: u_λ = u/λ, v_λ = v/λ. | ||
| */ | ||
| async generateUVCoverage() { | ||
| const stations = this.getStationPositions(); | ||
| if (!stations || stations.length < 2) { | ||
| this.updateStatus("Erro: São necessárias pelo menos 2 estações para calcular a cobertura UV."); | ||
| console.error("UVCoverageSimulator: Número insuficiente de estações."); | ||
| return; | ||
| } | ||
|
|
||
| this.updateStatus("Calculando cobertura UV..."); | ||
|
|
||
| // Aguarda WebGPU ficar pronto (se ainda estiver inicializando) | ||
| if (this._gpuReadyPromise) { | ||
| await this._gpuReadyPromise; | ||
| } | ||
|
|
||
| const params = this._readParams(); | ||
| const decRad = params.dec * BingoConstants.DEG_TO_RAD; | ||
| const lambda = BingoConstants.SPEED_OF_LIGHT / params.freqHz; | ||
| const sinDec = Math.sin(decRad); | ||
| const cosDec = Math.cos(decRad); | ||
|
|
||
| // Ângulos horários: centrados em 0, de -duration/2 a +duration/2 | ||
| const halfDuration = params.duration / 2; | ||
| const hourAngles = []; | ||
| for (let i = 0; i < params.timesteps; i++) { | ||
| const hHours = params.timesteps === 1 ? 0 : -halfDuration + (params.duration * i) / (params.timesteps - 1); | ||
| hourAngles.push(hHours * 15 * BingoConstants.DEG_TO_RAD); | ||
| } |
There was a problem hiding this comment.
O parâmetro latitude é lido em _readParams() e existe na UI, mas não é usado em nenhum cálculo (nem CPU nem GPU). Isso deixa o controle enganoso para o usuário. Sugestão: ou remover o campo/param, ou incorporar latitude na transformação padrão ENU→UVW (ou explicar explicitamente no UI que a aproximação atual ignora latitude).
| // Redimensiona o canvas do gerador e plots Plotly quando voltar à aba de layout | ||
| if (tabId === 'tab-layout') { | ||
| setTimeout(() => { | ||
| if (window.antennaGenerator?.resizeCanvas) { | ||
| window.antennaGenerator.resizeCanvas(); | ||
| } | ||
| // Redimensiona plots Plotly | ||
| const plotIds = ['beam-pattern-plot', 'psf-ee-theta-plot']; | ||
| plotIds.forEach(id => { | ||
| const el = document.getElementById(id); | ||
| if (el && typeof Plotly !== 'undefined') { | ||
| try { Plotly.Plots.resize(el); } catch(e) { /* plot pode não existir */ } | ||
| } | ||
| }); | ||
| }, 100); | ||
| } |
There was a problem hiding this comment.
Ao voltar para a aba tab-layout, o Leaflet normalmente precisa de invalidateSize() quando o container ficou display:none (abas). Aqui só há resize do canvas e dos plots Plotly; isso pode deixar o mapa em branco ou com tiles desalinhados após trocar de aba. Sugestão: no bloco tabId === 'tab-layout', chamar window.interactiveMap?.map?.invalidateSize(true) (ou expor um método no InteractiveMap).
Station parameter changes required manual button clicks to regenerate layouts. Export section showed all three coordinate formats (WGS84/ECEF/ENU) simultaneously without explaining their differences.
Real-time station generation
_autoGenerate()inStationManagerthat firesgenerateStations()on any parameter changestation-count,station-spacing,station-layout-type, and all dynamically-created extra params via_updateExtraParams()Coordinate format selector
WGS84, ECEF, and ENU are three distinct coordinate systems — not interchangeable:
layout_ecef.txtfor absolute positioninglayout_enu.txtfor relative positioning✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.