From 26240e9111dcaab029ec05084cd007ea477e53e4 Mon Sep 17 00:00:00 2001 From: MartaEY Date: Mon, 10 Nov 2025 22:52:32 +0100 Subject: [PATCH 01/28] avances sequia --- anemui-core/src/OpenLayersMap.ts | 61 ++++---- anemui-core/src/PaletteManager.ts | 188 +++++++++++++++++------- anemui-core/src/data/ChunkDownloader.ts | 41 ++++-- anemui-core/src/ui/MenuBar.tsx | 50 +++---- 4 files changed, 218 insertions(+), 122 deletions(-) diff --git a/anemui-core/src/OpenLayersMap.ts b/anemui-core/src/OpenLayersMap.ts index 79b76c9..018e75d 100644 --- a/anemui-core/src/OpenLayersMap.ts +++ b/anemui-core/src/OpenLayersMap.ts @@ -126,7 +126,6 @@ protected setExtents(timesJs: CsTimesJsData, varId: string): void { timesJs.latMax[selector] + pxSize / 2 ]; - // Si el mapa está en una proyección diferente, transformar if (timesJs.projection !== olProjection) { const transformedExtent = transformExtent( dataExtent, @@ -141,7 +140,7 @@ protected setExtents(timesJs: CsTimesJsData, varId: string): void { } - init(_parent: CsMap): void { +init(_parent: CsMap): void { this.parent = _parent; const state = this.parent.getParent().getState(); const timesJs = this.parent.getParent().getTimesJs(); @@ -149,25 +148,34 @@ protected setExtents(timesJs: CsTimesJsData, varId: string): void { this.setExtents(timesJs, state.varId); this.defaultRenderer = this.parent.getParent().getDefaultRenderer() this.renderers = this.parent.getParent().getRenderers() + let layers: (ImageLayer | TileLayer)[] = isWmsEnabled ? this.buildWmsLayers(state) : this.buildChunkLayers(state); let options: MapOptions = { - target: 'map', - layers: layers, - view: new View({ - center: center, - zoom: initialZoom, - projection: olProjection - }) + target: 'map', + layers: layers, + view: new View({ + center: center, + zoom: initialZoom, + projection: olProjection + }), + controls: [] }; this.map = new Map(options); + + + setTimeout(() => { + this.map.getControls().clear(); + }, 100); + let self = this; this.map.on('movestart', event => { self.onDragStart(event) }) this.map.on('loadend', () => { self.onMapLoaded() }) this.map.on('click', (event) => { self.onClick(event) }) this.map.on('moveend', self.handleMapMove.bind(this)); - this.marker = new Overlay({ + + this.marker = new Overlay({ positioning: 'center-center', element: document.createElement('div'), stopEvent: false, @@ -184,13 +192,13 @@ protected setExtents(timesJs: CsTimesJsData, varId: string): void { this.buildPopUp() this.map.on('pointermove', (event) => self.onMouseMove(event)) this.lastSupport = this.parent.getParent().getDefaultRenderer() + this.buildFeatureLayers(); if (!isWmsEnabled) { - this.buildDataTilesLayers(state, timesJs); - if (state.uncertaintyLayer) this.buildUncertaintyLayer(state, timesJs); + this.buildDataTilesLayers(state, timesJs); + if (state.uncertaintyLayer) this.buildUncertaintyLayer(state, timesJs); } - } - +} private buildWmsLayers(state: CsViewerData): (ImageLayer | TileLayer)[] { this.dataWMSLayer = new TileWMS({ url: '/geoserver/lcsc/wms', @@ -432,8 +440,6 @@ private shouldShowPercentileClock(state: CsViewerData): boolean { public buildDataTilesLayers(state: CsViewerData, timesJs: CsTimesJsData): void { let app = window.CsViewerApp; - - this.safelyRemoveDataLayers(); this.dataTilesLayer = []; @@ -445,15 +451,18 @@ public buildDataTilesLayers(state: CsViewerData, timesJs: CsTimesJsData): void { timesJs.portions[state.varId].forEach((portion: string, index, array) => { let imageLayer: ImageLayer = new ImageLayer({ - visible: true, + visible: true, opacity: 1.0, - zIndex: 100, - source: null + zIndex: 5000 + index, + source: null, + properties: { + 'name': `data-layer-${index}`, + 'portion': portion + } }); this.dataTilesLayer.push(imageLayer); - // Insertar la capa antes de la capa política (no al final) const layers = this.map.getLayers(); const politicalIndex = layers.getArray().indexOf(this.politicalLayer); if (politicalIndex !== -1) { @@ -461,16 +470,15 @@ public buildDataTilesLayers(state: CsViewerData, timesJs: CsTimesJsData): void { } else { layers.push(imageLayer); } - }); let promises: Promise[] = []; this.setExtents(timesJs, state.varId); if (computedDataTilesLayer) { - timesJs.portions[state.varId].forEach((portion: string) => { - promises.push(this.computeLayerData(state.selectedTimeIndex, state.varId, portion)); - }); + timesJs.portions[state.varId].forEach((portion: string) => { + promises.push(this.computeLayerData(state.selectedTimeIndex, state.varId, portion)); + }); } else { timesJs.portions[state.varId].forEach((portion: string, index, array) => { promises.push(downloadXYChunk(state.selectedTimeIndex, state.varId, portion, timesJs)); @@ -480,7 +488,7 @@ public buildDataTilesLayers(state: CsViewerData, timesJs: CsTimesJsData): void { if (this.dataTilesLayer.length > 0 && promises.length > 0) { buildImages(promises, this.dataTilesLayer, state, timesJs, app, this.ncExtents, false) .then(() => { - // FORZAR REFRESH COMPLETO + // Asegurar que las capas están visibles this.dataTilesLayer.forEach((layer, i) => { layer.setVisible(true); layer.changed(); @@ -496,7 +504,6 @@ public buildDataTilesLayers(state: CsViewerData, timesJs: CsTimesJsData): void { } } - // Safe layer removal method private safelyRemoveDataLayers(): void { if (this.dataTilesLayer && Array.isArray(this.dataTilesLayer)) { this.dataTilesLayer.forEach((layer: ImageLayer | TileLayer) => { @@ -520,12 +527,10 @@ public buildDataTilesLayers(state: CsViewerData, timesJs: CsTimesJsData): void { this.dataTilesLayer = []; } - // Fix for uncertainty layer with proper initialization public buildUncertaintyLayer(state: CsViewerData, timesJs: CsTimesJsData): void { let lmgr = LayerManager.getInstance(); let app = window.CsViewerApp; - // Safely remove existing uncertainty layers this.safelyRemoveUncertaintyLayers(); this.uncertaintyLayer = lmgr.getUncertaintyLayer() || []; diff --git a/anemui-core/src/PaletteManager.ts b/anemui-core/src/PaletteManager.ts index edd5f81..8a75568 100644 --- a/anemui-core/src/PaletteManager.ts +++ b/anemui-core/src/PaletteManager.ts @@ -170,78 +170,150 @@ export class CategoryRangePainter implements Painter { this.ranges = ranges; } - public async paintValues(floatArray: number[], width: number, height: number, minArray: number, maxArray: number, pxTransparent: number, uncertaintyLayer: boolean): Promise { - // Validar y asegurar que width y height sean enteros positivos válidos - width = Math.max(1, Math.floor(width)); - height = Math.max(1, Math.floor(height)); - - if (!isFinite(width) || !isFinite(height)) { - console.error('Invalid canvas dimensions:', width, height); - width = 1; - height = 1; - } - - let canvas: HTMLCanvasElement = document.createElement('canvas'); - let context: CanvasRenderingContext2D = canvas.getContext('2d'); - canvas.width = width; - canvas.height = height; - let imgData: ImageData = context.getImageData(0, 0, width, height); - let gradient = PaletteManager.getInstance().updatePalete32(uncertaintyLayer); +public async paintValues(floatArray: number[], width: number, height: number, minArray: number, maxArray: number, pxTransparent: number, uncertaintyLayer: boolean): Promise { + // Validar dimensiones + width = Math.max(1, Math.floor(width)); + height = Math.max(1, Math.floor(height)); + + if (!isFinite(width) || !isFinite(height)) { + console.error('Invalid canvas dimensions:', width, height); + width = 1; + height = 1; + } - // VERIFICACIÓN CRÍTICA - if (gradient.length !== this.ranges.length) { - console.error('❌ CRITICAL: Gradient colors (' + gradient.length + ') != Ranges (' + this.ranges.length + ')'); + // 🔍 DIAGNÓSTICO 1: Calidad de datos de entrada + let dataQuality = { + total: floatArray.length, + nan: 0, + infinite: 0, + valid: 0, + samples: [] as number[] + }; + + for (let i = 0; i < floatArray.length; i++) { + const val = floatArray[i]; + if (isNaN(val)) { + dataQuality.nan++; + } else if (!isFinite(val)) { + dataQuality.infinite++; + } else { + dataQuality.valid++; + // Guardar algunos samples + if (dataQuality.samples.length < 20) { + dataQuality.samples.push(val); + } } + } - const bitmap: Uint32Array = new Uint32Array(imgData.data.buffer); + console.log('📊 Data Quality Check:', { + total: dataQuality.total, + valid: dataQuality.valid + ` (${(dataQuality.valid / dataQuality.total * 100).toFixed(2)}%)`, + nan: dataQuality.nan, + infinite: dataQuality.infinite, + firstSamples: dataQuality.samples.slice(0, 10) + }); + + let canvas: HTMLCanvasElement = document.createElement('canvas'); + let context: CanvasRenderingContext2D = canvas.getContext('2d'); + canvas.width = width; + canvas.height = height; + let imgData: ImageData = context.getImageData(0, 0, width, height); + let gradient = PaletteManager.getInstance().updatePalete32(uncertaintyLayer); + + // VERIFICACIÓN + if (gradient.length !== this.ranges.length) { + console.error('❌ CRITICAL: Gradient colors (' + gradient.length + ') != Ranges (' + this.ranges.length + ')'); + } - for (let y: number = 0; y < height; y++) { - for (let x: number = 0; x < width; x++) { - let ncIndex: number = x + y * width; - let value: number = floatArray[ncIndex]; - let pxIndex: number = x + ((height - 1) - y) * width; + console.log('🎨 Ranges:', this.ranges); + console.log('🎨 Gradient length:', gradient.length); - if (!isNaN(value) && isFinite(value)) { - let index: number = this.getValIndex(value); + const bitmap: Uint32Array = new Uint32Array(imgData.data.buffer); - if (index >= 0 && index < gradient.length) { - bitmap[pxIndex] = gradient[index]; - } else { - bitmap[pxIndex] = pxTransparent; - } + // Debug stats + let debugStats = { + transparent: 0, + byRange: Array(this.ranges.length).fill(0), + minValue: Infinity, + maxValue: -Infinity, + indexReturned: {} as Record, // Contar qué índices retorna getValIndex + valuesOutOfRange: [] as number[] // Valores que no caen en ningún rango + }; + + // PINTAR Y RECOPILAR ESTADÍSTICAS + for (let y: number = 0; y < height; y++) { + for (let x: number = 0; x < width; x++) { + let ncIndex: number = x + y * width; + let value: number = floatArray[ncIndex]; + let pxIndex: number = x + ((height - 1) - y) * width; + + if (!isNaN(value) && isFinite(value)) { + // Actualizar estadísticas + debugStats.minValue = Math.min(debugStats.minValue, value); + debugStats.maxValue = Math.max(debugStats.maxValue, value); + + let index: number = this.getValIndex(value); + + // 🔍 DIAGNÓSTICO 2: Registrar qué índices se están retornando + if (!debugStats.indexReturned[index]) { + debugStats.indexReturned[index] = 0; + } + debugStats.indexReturned[index]++; + + if (index >= 0 && index < gradient.length) { + debugStats.byRange[index]++; + bitmap[pxIndex] = gradient[index]; } else { + debugStats.transparent++; bitmap[pxIndex] = pxTransparent; + + // 🔍 DIAGNÓSTICO 3: Guardar valores que caen fuera de rango + if (debugStats.valuesOutOfRange.length < 20) { + debugStats.valuesOutOfRange.push(value); + } } + } else { + bitmap[pxIndex] = pxTransparent; + debugStats.transparent++; } } - - context.putImageData(imgData, 0, 0); - return canvas; } + + // Mostrar estadísticas completas + console.log('🎨 Paint Stats:', { + dataRange: [debugStats.minValue, debugStats.maxValue], + totalPixels: width * height, + transparent: debugStats.transparent + ` (${(debugStats.transparent / (width * height) * 100).toFixed(2)}%)`, + pixelsByRange: debugStats.byRange, + indexReturned: debugStats.indexReturned, + valuesOutOfRange: debugStats.valuesOutOfRange.length > 0 ? debugStats.valuesOutOfRange : 'None' + }); + + context.putImageData(imgData, 0, 0); + return canvas; +} - getValIndex(val: number): number { - // Buscar el rango apropiado - for (let i = 0; i < this.ranges.length; i++) { - let range = this.ranges[i]; - - // Rango con límite inferior indefinido: val < b - if (range.a === undefined && val < range.b) { - return i; - } - // Rango con límite superior indefinido: val >= a - else if (range.b === undefined && val >= range.a) { - return i; - } - // Rango normal: a <= val < b - else if (range.a !== undefined && range.b !== undefined) { - if (val >= range.a && val < range.b) { - return i; - } - } - } +getValIndex(val: number): number { + if (isNaN(val) || !isFinite(val)) { + return -1; + } + + for (let i = 0; i < this.ranges.length; i++) { + let range = this.ranges[i]; + + const a = (typeof range.a === 'number') ? range.a : -Infinity; + const b = (typeof range.b === 'number') ? range.b : Infinity; - return -1; // No encontrado + const isLastRange = (i === this.ranges.length - 1); + + if (val >= a && (val < b || (isLastRange && val <= b))) { + return i; + } } + + // Si llegamos aquí, el valor no cayó en ningún rango + return -1; +} getColorString(val: number, min: number, max: number): string { let mgr = PaletteManager.getInstance(); @@ -254,6 +326,8 @@ export class CategoryRangePainter implements Painter { return "#000000"; } + + } export class GradientPainter implements Painter{ diff --git a/anemui-core/src/data/ChunkDownloader.ts b/anemui-core/src/data/ChunkDownloader.ts index 5e6dc71..3fc4464 100644 --- a/anemui-core/src/data/ChunkDownloader.ts +++ b/anemui-core/src/data/ChunkDownloader.ts @@ -275,12 +275,12 @@ export async function buildImages(promises: Promise[], dataTilesLayer: const validFloatArrays = floatArrays.filter(arr => arr !== undefined && arr !== null); if (validFloatArrays.length === 0) { + console.warn('No valid float arrays received'); return; } const actualTimeIndex = getActualTimeIndex(status.selectedTimeIndex, status.varId, timesJs); - if (!Array.isArray(timesJs.varMin[status.varId])) { timesJs.varMin[status.varId] = []; } @@ -291,7 +291,9 @@ export async function buildImages(promises: Promise[], dataTilesLayer: const filteredArrays: number[][] = []; for (let i = 0; i < validFloatArrays.length; i++) { - const filteredArray = await app.filterValues(validFloatArrays[i], actualTimeIndex, status.varId, timesJs.portions[status.varId][i]); + + const filteredArray = validFloatArrays[i]; + filteredArrays.push(filteredArray); } @@ -313,6 +315,7 @@ export async function buildImages(promises: Promise[], dataTilesLayer: maxArray = 100; } + try { (timesJs.varMin[status.varId] as number[])[actualTimeIndex] = minArray; (timesJs.varMax[status.varId] as number[])[actualTimeIndex] = maxArray; @@ -328,7 +331,6 @@ export async function buildImages(promises: Promise[], dataTilesLayer: let painterInstance = PaletteManager.getInstance().getPainter(); - for (let i = 0; i < filteredArrays.length; i++) { const filteredArray = filteredArrays[i]; @@ -351,11 +353,9 @@ export async function buildImages(promises: Promise[], dataTilesLayer: }); dataTilesLayer[i].setSource(imageSource); - dataTilesLayer[i].setZIndex(5000 + i); dataTilesLayer[i].setVisible(true); dataTilesLayer[i].setOpacity(1.0); - dataTilesLayer[i].changed(); await new Promise(resolve => setTimeout(resolve, 50)); @@ -371,7 +371,6 @@ export async function buildImages(promises: Promise[], dataTilesLayer: } } } - } } catch (error) { @@ -384,15 +383,12 @@ export async function buildImages(promises: Promise[], dataTilesLayer: if (window.CsViewerApp && (window.CsViewerApp as any).csMap) { const map = (window.CsViewerApp as any).csMap.map; if (map) { - map.render(); - await new Promise(resolve => setTimeout(resolve, 100)); - if (map.renderSync) { map.renderSync(); } - } else { + } else { console.warn('Map not available'); } } @@ -429,7 +425,6 @@ let xyCache: { } = undefined async function downloadXYChunkNC(t: number, varName: string, portion: string, timesJs: CsTimesJsData): Promise { - let app = window.CsViewerApp; const actualTimeIndex = getActualTimeIndex(t, varName, timesJs); @@ -466,18 +461,40 @@ async function downloadXYChunkNC(t: number, varName: string, portion: string, ti const chunk = await rangeRequest(ncUrl, BigInt(chunkOffset), BigInt(chunkOffset) + BigInt(chunkSize) - BigInt(1)); const uncompressedArray = inflate(chunk); - const floatArray = Array.from(chunkStruct.iter_unpack(uncompressedArray.buffer), x => x[0]); if (!Array.isArray(floatArray) || floatArray.length === 0) { throw new Error(`Invalid float array: length=${floatArray.length}, isArray=${Array.isArray(floatArray)}`); } + // 🔍 DEBUG: Verificar calidad de datos descargados + const validCount = floatArray.filter(v => !isNaN(v) && isFinite(v)).length; + console.log('🔍 downloadXYChunkNC OUTPUT:', { + varName, + portion, + actualTimeIndex, + total: floatArray.length, + valid: validCount, + validPercent: (validCount / floatArray.length * 100).toFixed(2) + '%', + samples: floatArray.slice(0, 20), + min: Math.min(...floatArray.filter(v => !isNaN(v) && isFinite(v))), + max: Math.max(...floatArray.filter(v => !isNaN(v) && isFinite(v))) + }); + xyCache = { t: actualTimeIndex, varName, portion, data: [...floatArray] }; let ret = [...floatArray]; app.transformDataXY(ret, actualTimeIndex, varName, portion); + // 🔍 DEBUG: Verificar datos después de transformDataXY + const validAfterTransform = ret.filter(v => !isNaN(v) && isFinite(v)).length; + console.log('🔍 After transformDataXY:', { + varName, + portion, + valid: validAfterTransform, + validPercent: (validAfterTransform / ret.length * 100).toFixed(2) + '%', + samples: ret.slice(0, 20) + }); return ret; diff --git a/anemui-core/src/ui/MenuBar.tsx b/anemui-core/src/ui/MenuBar.tsx index 1c74b79..0088748 100644 --- a/anemui-core/src/ui/MenuBar.tsx +++ b/anemui-core/src/ui/MenuBar.tsx @@ -367,32 +367,32 @@ export class MenuBar extends BaseFrame { addChild(this.displayUnits, this.units.render(this.parent.getState().subVarName, false)); this.units.build(this.displayUnits); } - if (hasClimatology) { - console.log('Building climatology dropdowns, extraDisplays:', this.extraDisplays.length); - - this.extraDisplays.forEach((dsp) => { - console.log('Processing display:', dsp.role, dsp.title, dsp.subTitle); - addChild(this.inputsFrame, this.renderDisplay(dsp, 'climBtn')); - addChild(this.inputsFrameMobile, this.renderDisplay(dsp, 'climBtn')); - this.extraMenuItems.forEach((dpn) => { - if (dpn.id == dsp.role) { - console.log(' -> Rendering menu item:', dpn.id); - let container: HTMLDivElement = document.querySelector("[role=" + dsp.role + "]") - addChild(container, dpn.render(dsp.subTitle, false)); - dpn.build(container) - } - }); - if (this.extraMenuInputs.length > 0) { - this.extraMenuInputs.forEach((input) => { - if (input.id == dsp.role) { - let container: HTMLDivElement = document.querySelector("[role=" + dsp.role + "]") - addChild(container, input.render(this.parent.getState().selectionParam + '')); - input.build(container) - } - }) - } - }); + if (hasClimatology) { + + + this.extraDisplays.forEach((dsp) => { + addChild(this.inputsFrame, this.renderDisplay(dsp, 'basicBtn')); + addChild(this.inputsFrameMobile, this.renderDisplay(dsp, 'basicBtn')); + + this.extraMenuItems.forEach((dpn) => { + if (dpn.id == dsp.role) { + console.log(' -> Rendering menu item:', dpn.id); + let container: HTMLDivElement = document.querySelector("[role=" + dsp.role + "]") + addChild(container, dpn.render(dsp.subTitle, false)); + dpn.build(container) } + }); + if (this.extraMenuInputs.length > 0) { + this.extraMenuInputs.forEach((input) => { + if (input.id == dsp.role) { + let container: HTMLDivElement = document.querySelector("[role=" + dsp.role + "]") + addChild(container, input.render(this.parent.getState().selectionParam + '')); + input.build(container) + } + }) + } + }); +} if (this.dropDownOrder.length) { this.changeMenuItemOrder() } From 50dc2131d29c58d9e73bfade17ae3a90e557f964 Mon Sep 17 00:00:00 2001 From: ManuelPruebas Date: Fri, 28 Nov 2025 09:23:30 +0100 Subject: [PATCH 02/28] =?UTF-8?q?Correcci=C3=B3n=20colores=20capas=20calcu?= =?UTF-8?q?ladas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- anemui-core/src/PaletteManager.ts | 37 +++++++++++--- anemui-core/src/data/ChunkDownloader.ts | 66 +++++++++++++++++++------ 2 files changed, 82 insertions(+), 21 deletions(-) diff --git a/anemui-core/src/PaletteManager.ts b/anemui-core/src/PaletteManager.ts index bf19827..b445e64 100644 --- a/anemui-core/src/PaletteManager.ts +++ b/anemui-core/src/PaletteManager.ts @@ -273,10 +273,10 @@ export class GradientPainter implements Painter{ .setMidpoint(points) .getColors(); } - - public async paintValues(floatArray: number[], width: number, height: number, minArray: number, maxArray: number, pxTransparent: number,uncertaintyLayer:boolean): Promise { + + public async paintValues(floatArray: number[], width: number, height: number, minArray: number, maxArray: number, pxTransparent: number, uncertaintyLayer: boolean): Promise { let canvas: HTMLCanvasElement = document.createElement('canvas'); - let context: CanvasRenderingContext2D = canvas.getContext('2d'); + let context: CanvasRenderingContext2D = canvas.getContext('2d'); canvas.width = width; canvas.height = height; let imgData: ImageData = context.getImageData(0, 0, width, height); @@ -327,7 +327,25 @@ export class GradientPainter implements Painter{ } export class CsDynamicPainter implements Painter{ - + // Variable privada para almacenar breaks precalculados + private precalculatedBreaks: number[] | null = null; + + /** + * Método para establecer breaks precalculados (usado cuando se necesita calcular breaks + * con datos combinados de múltiples porciones) + */ + public setPrecalculatedBreaks(dataForBreaks: number[]): void { + let niceSteps = new NiceSteps(); + this.precalculatedBreaks = niceSteps.getRegularSteps(dataForBreaks.filter(v => !isNaN(v)), maxPaletteSteps, maxPaletteValue); + } + + /** + * Método para limpiar los breaks precalculados + */ + public clearPrecalculatedBreaks(): void { + this.precalculatedBreaks = null; + } + public getColorString(val: number, min: number, max: number): string { let mgr = PaletteManager.getInstance() let paletteStr: string[] = mgr.updatePaletteStrings() @@ -351,9 +369,14 @@ export class CsDynamicPainter implements Painter{ let gradient = PaletteManager.getInstance().updatePalete32(uncertaintyLayer); let gradientLength = gradient.length - 1; - // Obtener los breaks de NiceSteps - let niceSteps = new NiceSteps(); - let breaks = niceSteps.getRegularSteps(floatArray.filter(v => !isNaN(v)), maxPaletteSteps, maxPaletteValue); + // Usar breaks precalculados si existen, si no calcularlos con floatArray + let breaks: number[]; + if (this.precalculatedBreaks !== null) { + breaks = this.precalculatedBreaks; + } else { + let niceSteps = new NiceSteps(); + breaks = niceSteps.getRegularSteps(floatArray.filter(v => !isNaN(v)), maxPaletteSteps, maxPaletteValue); + } const bitmap: Uint32Array = new Uint32Array(imgData.data.buffer); diff --git a/anemui-core/src/data/ChunkDownloader.ts b/anemui-core/src/data/ChunkDownloader.ts index 239ecbd..9fe344e 100644 --- a/anemui-core/src/data/ChunkDownloader.ts +++ b/anemui-core/src/data/ChunkDownloader.ts @@ -298,14 +298,50 @@ export async function buildImages(promises: Promise[], dataTilesLayer: let minArray: number = Number.MAX_VALUE; let maxArray: number = Number.MIN_VALUE; - filteredArrays.forEach((filteredArray) => { - filteredArray.forEach((value) => { - if (!isNaN(value) && isFinite(value)) { - minArray = Math.min(minArray, value); - maxArray = Math.max(maxArray, value); - } + // Para datos computados, calcular min/max usando todos los datos (península + canarias) + let allValidNumbers: number[] = []; + if (status.computedLayer) { + // Obtener datos de península y filtrar NaN + const penData = status.computedData['_pen']; + if (Array.isArray(penData)) { + const validNumbersInArray = penData.filter(num => + typeof num === 'number' && !isNaN(num) && isFinite(num) + ); + allValidNumbers.push(...validNumbersInArray); + } + + // Obtener datos de canarias y filtrar NaN + const canData = status.computedData['_can']; + if (Array.isArray(canData)) { + const validNumbersInArray = canData.filter(num => + typeof num === 'number' && !isNaN(num) && isFinite(num) + ); + allValidNumbers.push(...validNumbersInArray); + } + + // Calcular min/max con todos los datos válidos + allValidNumbers.forEach((value) => { + minArray = Math.min(minArray, value); + maxArray = Math.max(maxArray, value); }); - }); + + // Para datos computados con rango muy pequeño (ej: probabilidades todas iguales), + // forzar un rango fijo 0-1 para permitir el pintado + if ((maxArray - minArray) < 0.01) { + minArray = 0; + maxArray = 1; + } + } else { + // Para datos NO computados (leídos de fichero), usar filteredArrays + filteredArrays.forEach((filteredArray) => { + filteredArray.forEach((value) => { + if (!isNaN(value) && isFinite(value)) { + minArray = Math.min(minArray, value); + maxArray = Math.max(maxArray, value); + } + }); + }); + } if (minArray === Number.MAX_VALUE || maxArray === Number.MIN_VALUE) { console.warn('No valid data found, using default ranges'); @@ -313,13 +349,6 @@ export async function buildImages(promises: Promise[], dataTilesLayer: maxArray = 100; } - // Para datos computados con rango muy pequeño (ej: probabilidades todas iguales), - // forzar un rango fijo 0-1 para permitir el pintado - if (status.computedLayer && (maxArray - minArray) < 0.01) { - minArray = 0; - maxArray = 1; - } - try { (timesJs.varMin[status.varId] as number[])[actualTimeIndex] = minArray; (timesJs.varMax[status.varId] as number[])[actualTimeIndex] = maxArray; @@ -335,6 +364,10 @@ export async function buildImages(promises: Promise[], dataTilesLayer: let painterInstance = PaletteManager.getInstance().getPainter(); + // Para datos computados, precalcular breaks con todos los datos combinados + if (status.computedLayer && allValidNumbers.length > 0 && (painterInstance as any).setPrecalculatedBreaks) { + (painterInstance as any).setPrecalculatedBreaks(allValidNumbers); + } for (let i = 0; i < filteredArrays.length; i++) { const filteredArray = filteredArrays[i]; @@ -387,6 +420,11 @@ export async function buildImages(promises: Promise[], dataTilesLayer: } } + // Limpiar breaks precalculados después de pintar todas las porciones + if (status.computedLayer && (painterInstance as any).clearPrecalculatedBreaks) { + (painterInstance as any).clearPrecalculatedBreaks(); + } + try { if (window.CsViewerApp && (window.CsViewerApp as any).csMap) { const map = (window.CsViewerApp as any).csMap.map; From 1f9f4deea91b1724af36694a393566b445651eee Mon Sep 17 00:00:00 2001 From: ManuelPruebas Date: Fri, 28 Nov 2025 13:02:44 +0100 Subject: [PATCH 03/28] =?UTF-8?q?CsMenuInput:=20nuevo=20m=C3=A9todo=20para?= =?UTF-8?q?=20reseteo=20de=20input?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- anemui-core/src/ui/CsMenuItem.tsx | 11 +++++++++++ anemui-core/src/ui/MenuBar.tsx | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/anemui-core/src/ui/CsMenuItem.tsx b/anemui-core/src/ui/CsMenuItem.tsx index d8028e8..9776631 100644 --- a/anemui-core/src/ui/CsMenuItem.tsx +++ b/anemui-core/src/ui/CsMenuItem.tsx @@ -249,6 +249,17 @@ export class CsMenuInput extends BaseUiElement { return this.title } + public setValue(_value: number) { + this.value = _value; + // Actualizar también el valor del input HTML renderizado + if (this.container) { + const inputElement = this.container.querySelector(`#${this.id}`) as HTMLInputElement; + if (inputElement) { + inputElement.value = _value.toString(); + } + } + } + private validateValue(inputValue: number): number | null { /* if (isNaN(inputValue)) { return null; // Permitir campo vacío diff --git a/anemui-core/src/ui/MenuBar.tsx b/anemui-core/src/ui/MenuBar.tsx index 9e178a1..bfa6bb0 100644 --- a/anemui-core/src/ui/MenuBar.tsx +++ b/anemui-core/src/ui/MenuBar.tsx @@ -682,7 +682,7 @@ export class MenuBar extends BaseFrame { if (options && options.length > 0 && options[0] !== undefined && options[0] !== '') { const newValue = parseFloat(options[0]); if (!isNaN(newValue)) { - inp.value = newValue; + inp.setValue(newValue); } } // Controlar visibilidad usando el método config From af71eaf39684da5b04211e9fa1fed9da0ff84bf2 Mon Sep 17 00:00:00 2001 From: MartaEY Date: Fri, 28 Nov 2025 14:04:24 +0100 Subject: [PATCH 04/28] avances sequia --- anemui-core/src/OpenLayersMap.ts | 326 ++++++++++++++++-------- anemui-core/src/PaletteManager.ts | 10 +- anemui-core/src/data/ChunkDownloader.ts | 67 ++++- 3 files changed, 277 insertions(+), 126 deletions(-) diff --git a/anemui-core/src/OpenLayersMap.ts b/anemui-core/src/OpenLayersMap.ts index 018e75d..2412248 100644 --- a/anemui-core/src/OpenLayersMap.ts +++ b/anemui-core/src/OpenLayersMap.ts @@ -12,7 +12,7 @@ import { PaletteManager } from "./PaletteManager"; import { isTileDebugEnabled, isWmsEnabled, olProjection, initialZoom, computedDataTilesLayer } from "./Env"; import proj4 from 'proj4'; import { register } from 'ol/proj/proj4.js'; -import { buildImages, downloadXYChunk, CsvDownloadDone, downloadXYbyRegion, getPortionForPoint, downloadHistoricalDataForPercentile, calcPixelIndex, downloadTArrayChunked } from "./data/ChunkDownloader"; +import { buildImages, downloadXYChunk, CsvDownloadDone, downloadXYbyRegion, getPortionForPoint, downloadHistoricalDataForPercentile, calcPixelIndex, downloadTArrayChunked, downloadXYbyRegionMultiPortion } from "./data/ChunkDownloader"; import VectorSource from "ol/source/Vector"; import VectorLayer from "ol/layer/Vector"; import { Circle as CircleStyle, Fill, Stroke, Style } from 'ol/style.js'; @@ -23,6 +23,7 @@ import { LayerManager } from "./LayerManager"; import { loadLatLogValue, loadGeoJsonData } from "./data/CsDataLoader"; import DataTileSource from "ol/source/DataTile"; import { Geometry } from 'ol/geom'; +import { DataServiceApp } from "./ServiceApp"; // import { renderers, defaultRenderer, getFolders } from "./tiles/Support"; // Define alternative projections @@ -195,8 +196,8 @@ init(_parent: CsMap): void { this.buildFeatureLayers(); if (!isWmsEnabled) { - this.buildDataTilesLayers(state, timesJs); - if (state.uncertaintyLayer) this.buildUncertaintyLayer(state, timesJs); + this.buildDataTilesLayers(state, timesJs); + if (this.uncertaintyLayer) this.buildUncertaintyLayer(state, timesJs); } } private buildWmsLayers(state: CsViewerData): (ImageLayer | TileLayer)[] { @@ -527,54 +528,84 @@ public buildDataTilesLayers(state: CsViewerData, timesJs: CsTimesJsData): void { this.dataTilesLayer = []; } - public buildUncertaintyLayer(state: CsViewerData, timesJs: CsTimesJsData): void { +public buildUncertaintyLayer(state: CsViewerData, timesJs: CsTimesJsData): void { let lmgr = LayerManager.getInstance(); let app = window.CsViewerApp; + // Safely remove existing uncertainty layers this.safelyRemoveUncertaintyLayers(); - this.uncertaintyLayer = lmgr.getUncertaintyLayer() || []; - const uncertaintyVarId = state.varId + '_uncertainty'; + + console.log('🔍 Building uncertainty layer for:', uncertaintyVarId); + console.log('Available portions:', timesJs.portions[uncertaintyVarId]); + if (!timesJs.portions[uncertaintyVarId]) { - console.warn('No uncertainty portions found for varId:', uncertaintyVarId); - return; + console.warn('No uncertainty portions found for varId:', uncertaintyVarId); + return; } + this.uncertaintyLayer = []; + timesJs.portions[uncertaintyVarId].forEach((portion: string, index, array) => { - let imageLayer: ImageLayer = new ImageLayer({ - visible: true, - opacity: 1.0, - zIndex: 100, - source: null - }); + console.log(`Creating uncertainty layer for portion: ${portion}`); + + let imageLayer: ImageLayer = new ImageLayer({ + visible: true, + opacity: 1.0, + zIndex: 5001 + index, // IMPORTANTE: zIndex mayor que las capas de datos + source: null, + properties: { + 'name': `uncertainty-layer-${index}`, + 'portion': portion + } + }); - if (imageLayer) { this.uncertaintyLayer.push(imageLayer); - // Insertar antes de la capa política + + // Insertar DESPUÉS de las capas de datos const layers = this.map.getLayers(); - const politicalIndex = layers.getArray().indexOf(this.politicalLayer); - if (politicalIndex !== -1) { - layers.insertAt(politicalIndex, imageLayer); + const dataLayerIndex = layers.getArray().findIndex(l => + l.getProperties()['name']?.startsWith('data-layer') + ); + + if (dataLayerIndex !== -1) { + // Insertar justo después de las capas de datos + layers.insertAt(dataLayerIndex + 1 + index, imageLayer); } else { - const insertIndex = Math.max(0, layers.getLength() - 1); - layers.insertAt(insertIndex, imageLayer); + layers.push(imageLayer); } - } }); let promises: Promise[] = []; this.setExtents(timesJs, uncertaintyVarId); timesJs.portions[uncertaintyVarId].forEach((portion: string, index, array) => { - promises.push(downloadXYChunk(state.selectedTimeIndex, uncertaintyVarId, portion, timesJs)); + console.log(`Downloading uncertainty data for portion: ${portion}`); + promises.push(downloadXYChunk(state.selectedTimeIndex, uncertaintyVarId, portion, timesJs)); }); if (this.uncertaintyLayer.length > 0 && promises.length > 0) { - buildImages(promises, this.uncertaintyLayer, state, timesJs, app, this.ncExtents, true); - lmgr.showUncertaintyLayer(false); + console.log('Building uncertainty images...'); + buildImages(promises, this.uncertaintyLayer, state, timesJs, app, this.ncExtents, true) + .then(() => { + console.log('✅ Uncertainty layers built successfully'); + // Asegurar visibilidad + this.uncertaintyLayer.forEach((layer, i) => { + layer.setVisible(true); + layer.changed(); + console.log(`Uncertainty layer ${i} visible:`, layer.getVisible()); + }); + + // Forzar render + this.map.render(); + this.map.renderSync(); + }) + .catch(error => { + console.error('❌ Error building uncertainty images:', error); + }); } - } +} private safelyRemoveUncertaintyLayers(): void { if (this.uncertaintyLayer && Array.isArray(this.uncertaintyLayer)) { @@ -635,7 +666,7 @@ public buildDataTilesLayers(state: CsViewerData, timesJs: CsTimesJsData): void { // Safely remove uncertainty layers this.safelyRemoveUncertaintyLayers(); - if (state.uncertaintyLayer) { + if (this.uncertaintyLayer) { this.buildUncertaintyLayer(state, this.parent.getParent().getTimesJs()); } } @@ -644,49 +675,51 @@ public buildDataTilesLayers(state: CsViewerData, timesJs: CsTimesJsData): void { } } - // Enhanced updateRender with better error handling - async updateRender(support: string): Promise { +async updateRender(support: string): Promise { try { - let state = this.parent.getParent().getState(); - - // Safely hide existing layers - if (this.featureLayer && typeof this.featureLayer.hide === 'function') { - this.featureLayer.hide(); - this.featureLayer = null; - } - - if (this.contourLayer && typeof this.contourLayer.hide === 'function') { - this.contourLayer.hide(); - this.contourLayer = null; - } - - switch (support) { - case this.renderers[1]: - break; + + let state = this.parent.getParent().getState(); - case this.renderers[0]: - await this.setupStationRenderer(state, support); - break; + // Ocultar capas existentes + if (this.featureLayer && typeof this.featureLayer.hide === 'function') { + this.featureLayer.hide(); + this.featureLayer = null; + } - case this.renderers[2]: - case this.renderers[3]: - case this.renderers[4]: - case this.renderers[5]: - await this.setupRegionRenderer(state, support); - break; + if (this.contourLayer && typeof this.contourLayer.hide === 'function') { + this.contourLayer.hide(); + this.contourLayer = null; + } - default: - console.error("Render " + support + " not supported"); - return; - } + // Normalizar el nombre del renderer + const normalizedSupport = support.toLowerCase(); + + // Determinar el tipo de renderer + if (normalizedSupport.includes('rejilla')) { + console.log("Setting up Rejilla (grid) renderer"); + // Ya está cargada la rejilla + } + else if (normalizedSupport.includes('provincia')) { + await this.setupRegionRenderer(state, support); + } + else if (normalizedSupport.includes('ccaa') || normalizedSupport.includes('autonomia')) { + await this.setupRegionRenderer(state, support); + } + else if (normalizedSupport.includes('puntual') || normalizedSupport.includes('estacion')) { + await this.setupStationRenderer(state, support); + } + else { + console.error("Available renderers:", this.renderers); + return; + } - this.lastSupport = support; - await this.finalizeRenderUpdate(); + this.lastSupport = support; + await this.finalizeRenderUpdate(); } catch (error) { - console.error('Error in updateRender:', error); + console.error('Stack:', error.stack); } - } +} private async setupStationRenderer(state: CsViewerData, support: string): Promise { this.safelyRemoveDataLayers(); @@ -720,12 +753,14 @@ public buildDataTilesLayers(state: CsViewerData, timesJs: CsTimesJsData): void { }); } - private async setupRegionRenderer(state: CsViewerData, support: string): Promise { + private async setupRegionRenderer(state: CsViewerData, support: string): Promise { + this.safelyRemoveDataLayers(); let folders = this.parent.getParent().getFolders(support); + if (!folders || folders.length === 0) { - throw new Error(`No folders found for support: ${support}`); + throw new Error(`No folders found for support: ${support}`); } let dataFolder = this.selectDataFolder(folders); @@ -733,14 +768,46 @@ public buildDataTilesLayers(state: CsViewerData, timesJs: CsTimesJsData): void { this.featureLayer = this.glmgr.getGeoLayer(dataFolder); if (this.featureLayer) { - this.featureLayer.indexData = null; - let times = typeof (state.times) === 'string' ? state.times : state.times[state.selectedTimeIndex]; - await this.initializeFeatureLayer(times, state.selectedTimeIndex, dataFolder, state.varId); - this.setupInteractions(); + this.featureLayer.indexData = null; + let times = typeof (state.times) === 'string' ? state.times : state.times[state.selectedTimeIndex]; + + await this.initializeFeatureLayerMultiPortion(times, state.selectedTimeIndex, dataFolder, state.varId); + this.setupInteractions(); } else { - console.warn("featureLayer is undefined for dataFolder:", dataFolder); + console.error("Available geoLayers:", Object.keys(this.glmgr['geoLayers'])); } - } +} + +async initializeFeatureLayerMultiPortion( + time: string, + timeIndex: number, + folder: string, + varName: string +): Promise { + return new Promise((resolve, reject) => { + const portions = ["_pen", "_can"]; + + + let openSt: CsvDownloadDone = (data: any, filename: string, type: string) => { + if (this.featureLayer) { + this.featureLayer.indexData = data; + + if (this.featureLayer.show) { + this.featureLayer.show(this.renderers.indexOf(this.lastSupport)); + } + resolve(); + } else { + reject("featureLayer is undefined"); + } + }; + + if (computedDataTilesLayer) { + this.computeFeatureLayerData(time, folder, varName, openSt); + } else { + downloadXYbyRegionMultiPortion(time, timeIndex, folder, varName, portions, openSt); + } + }); +} private selectDataFolder(folders: string[]): string { if (folders.length <= 1) { @@ -1147,15 +1214,15 @@ export class CsOpenLayerGeoJsonLayer extends CsGeoJsonLayer { return new Style({ image: imgStation, }) } - public setFeatureStyle(state: CsViewerData, feature: Feature, timesJs: CsTimesJsData): Style { +public setFeatureStyle(state: CsViewerData, feature: Feature, timesJs: CsTimesJsData): Style { let min: number = Number.MAX_VALUE; let max: number = Number.MIN_VALUE; Object.values(this.indexData).forEach((value) => { - if (!isNaN(value)) { - min = Math.min(min, value); - max = Math.max(max, value); - } + if (!isNaN(value)) { + min = Math.min(min, value); + max = Math.max(max, value); + } }); let color: string = '#fff'; @@ -1163,28 +1230,43 @@ export class CsOpenLayerGeoJsonLayer extends CsGeoJsonLayer { let id_ant = feature.getProperties()['id_ant']; let ptr = PaletteManager.getInstance().getPainter(); - // Use exact match instead of includes to avoid matching '1' with '10', '11', etc. - // Try with new id first, fallback to old id_ant if not found - let dataValue = this.indexData[id]; + let mappedId = id; + if (id && id.length > 5) { + const match = id.match(/^34(\d{2})0000000$/); + if (match) { + mappedId = parseInt(match[1]).toString(); + console.log('🔄 Mapped ID:', id, '→', mappedId); + } + } + + let dataValue = this.indexData[mappedId]; + + if (dataValue === undefined) { + dataValue = this.indexData[id]; + } + if (dataValue === undefined && id_ant !== undefined) { - dataValue = this.indexData[id_ant]; + dataValue = this.indexData[id_ant]; } if (dataValue !== undefined) { - color = ptr.getColorString(dataValue, min, max); + color = ptr.getColorString(dataValue, min, max); + } else { + console.warn(' No data found for IDs:', id, mappedId, id_ant); } const isHovered = feature.get('hover'); if (isHovered) this.map.getTargetElement().style.cursor = 'pointer'; else this.map.getTargetElement().style.cursor = ''; + return new Style({ - fill: new Fill({ color: isHovered ? this.highLightColor(color, 0.2) : color }), - stroke: new Stroke({ - color: '#999', - }), + fill: new Fill({ color: isHovered ? this.highLightColor(color, 0.2) : color }), + stroke: new Stroke({ + color: '#999', + }), }); - } +} private intersectsExtent(featureExtent: number[], viewExtent: number[]): boolean { return !( @@ -1195,39 +1277,67 @@ export class CsOpenLayerGeoJsonLayer extends CsGeoJsonLayer { ); } - public showFeatureValue(data: any, feature: any, pixel: any, pos: CsLatLong): void { +public showFeatureValue(data: any, feature: any, pixel: any, pos: CsLatLong): void { let state: CsViewerData = this.csMap.getParent().getParent().getState(); let timesJs = this.csMap.getParent().getParent().getTimesJs(); let value: string; + if (feature) { - if (state.support == this.csMap.renderers[0]) feature.setStyle(this.setStationStyle(state, feature, timesJs)) - else feature.setStyle(this.setFeatureStyle(state, feature, timesJs)); - this.csMap.popupContent.style.left = pixel[0] + 'px'; - this.csMap.popupContent.style.top = pixel[1] + 'px'; - this.csMap.popup.hidden = false - if (feature !== this.currentFeature) { - let id = feature.getProperties()['id'] - let id_ant = feature.getProperties()['id_ant'] - // Use exact match instead of includes to avoid matching '1' with '10', '11', etc. - // Try with new id first, fallback to old id_ant if not found - value = data[id]; - if (value === undefined && id_ant !== undefined) { - value = data[id_ant]; + if (state.support == this.csMap.renderers[0]) { + feature.setStyle(this.setStationStyle(state, feature, timesJs)); + } else { + feature.setStyle(this.setFeatureStyle(state, feature, timesJs)); + } + + this.csMap.popupContent.style.left = pixel[0] + 'px'; + this.csMap.popupContent.style.top = pixel[1] + 'px'; + this.csMap.popup.hidden = false; + + if (feature !== this.currentFeature) { + let id = feature.getProperties()['id']; + let id_ant = feature.getProperties()['id_ant']; + + let mappedId = id; + if (id && id.length > 5) { + const match = id.match(/^34(\d{2})0000000$/); + if (match) { + mappedId = parseInt(match[1]).toString(); + } + } + + value = data[mappedId]; + + if (value === undefined) { + value = data[id]; + } + + if (value === undefined && id_ant !== undefined) { + value = data[id_ant]; + } + + if (value !== undefined && !isNaN(parseFloat(value))) { + this.csMap.popupContent.style.visibility = 'visible'; + this.csMap.popupContent.innerText = feature.get('name') + ': ' + parseFloat(value).toFixed(2); + this.csMap.value.setPosition(proj4('EPSG:4326', olProjection, [pos.lng, pos.lat])); + } else { + console.warn('No valid value for tooltip. ID:', id, 'mappedId:', mappedId, 'value:', value); + this.csMap.popupContent.style.visibility = 'hidden'; + } } - this.csMap.popupContent.style.visibility = 'visible'; - this.csMap.popupContent.innerText = feature.get('name') + ': ' + parseFloat(value).toFixed(2); - this.csMap.value.setPosition(proj4('EPSG:4326', olProjection, [pos.lng, pos.lat])) - } } else { - this.csMap.popupContent.style.visibility = 'hidden'; - this.csMap.popup.hidden = true + this.csMap.popupContent.style.visibility = 'hidden'; + this.csMap.popup.hidden = true; } + if (this.currentFeature instanceof Feature) { - if (state.support == this.csMap.renderers[0]) this.currentFeature.setStyle(this.setStationStyle(state, this.currentFeature, timesJs)) - else this.currentFeature.setStyle(this.setFeatureStyle(state, this.currentFeature, timesJs)); + if (state.support == this.csMap.renderers[0]) { + this.currentFeature.setStyle(this.setStationStyle(state, this.currentFeature, timesJs)); + } else { + this.currentFeature.setStyle(this.setFeatureStyle(state, this.currentFeature, timesJs)); + } } this.currentFeature = feature; - }; +} public highLightColor(hex: string, lum: number): string { hex = String(hex).replace(/[^0-9a-f]/gi, ''); diff --git a/anemui-core/src/PaletteManager.ts b/anemui-core/src/PaletteManager.ts index 8a75568..1caf1c6 100644 --- a/anemui-core/src/PaletteManager.ts +++ b/anemui-core/src/PaletteManager.ts @@ -220,13 +220,9 @@ public async paintValues(floatArray: number[], width: number, height: number, mi let imgData: ImageData = context.getImageData(0, 0, width, height); let gradient = PaletteManager.getInstance().updatePalete32(uncertaintyLayer); - // VERIFICACIÓN - if (gradient.length !== this.ranges.length) { - console.error('❌ CRITICAL: Gradient colors (' + gradient.length + ') != Ranges (' + this.ranges.length + ')'); - } - - console.log('🎨 Ranges:', this.ranges); - console.log('🎨 Gradient length:', gradient.length); + console.log('Gradient (colors) available:', gradient.length); + console.log('Ranges available:', this.ranges.length); + const bitmap: Uint32Array = new Uint32Array(imgData.data.buffer); diff --git a/anemui-core/src/data/ChunkDownloader.ts b/anemui-core/src/data/ChunkDownloader.ts index 3fc4464..ec25cfc 100644 --- a/anemui-core/src/data/ChunkDownloader.ts +++ b/anemui-core/src/data/ChunkDownloader.ts @@ -467,7 +467,6 @@ async function downloadXYChunkNC(t: number, varName: string, portion: string, ti throw new Error(`Invalid float array: length=${floatArray.length}, isArray=${Array.isArray(floatArray)}`); } - // 🔍 DEBUG: Verificar calidad de datos descargados const validCount = floatArray.filter(v => !isNaN(v) && isFinite(v)).length; console.log('🔍 downloadXYChunkNC OUTPUT:', { varName, @@ -486,16 +485,6 @@ async function downloadXYChunkNC(t: number, varName: string, portion: string, ti let ret = [...floatArray]; app.transformDataXY(ret, actualTimeIndex, varName, portion); - // 🔍 DEBUG: Verificar datos después de transformDataXY - const validAfterTransform = ret.filter(v => !isNaN(v) && isFinite(v)).length; - console.log('🔍 After transformDataXY:', { - varName, - portion, - valid: validAfterTransform, - validPercent: (validAfterTransform / ret.length * 100).toFixed(2) + '%', - samples: ret.slice(0, 20) - }); - return ret; } catch (error) { @@ -729,6 +718,62 @@ export function downloadXYbyRegion(time: string, timeIndex: number, folder: stri }, undefined, 'text'); } +export function downloadXYbyRegionMultiPortion( + time: string, + timeIndex: number, + folder: string, + varName: string, + portions: string[], + doneCb: (mergedData: any, filename: string, type: string) => void +) { + + const promises = portions.map(portion => { + return new Promise((resolve, reject) => { + const csvPath = `./regData/${folder}/${varName}${portion}.csv`; + console.log(" 📥 Downloading:", csvPath); + + downloadUrl(csvPath, (status: number, response) => { + if (status == 200) { + try { + const records = parse(response as Buffer, { + columns: true, + skip_empty_lines: true + }); + resolve({ portion, data: records[timeIndex] || {} }); + } catch (e) { + console.error(`Error parsing CSV ${varName}${portion}:`, e); + reject(e); + } + } else { + console.error(`HTTP ${status} for ${csvPath}`); + reject(new Error(`HTTP ${status}`)); + } + }, undefined, 'text'); + }); + }); + + Promise.all(promises) + .then((results: any[]) => { + + const mergedData: any = {}; + results.forEach(({ portion, data }) => { + Object.keys(data).forEach(key => { + if (key !== 'times_ini' && key !== 'times_end' && key !== 'times_mean') { + if (!mergedData[key] || isNaN(mergedData[key])) { + mergedData[key] = data[key]; + } + } + }); + }); + + doneCb(mergedData, varName, 'text/plain'); + }) + .catch(error => { + console.error("Error loading region portions:", error); + doneCb({}, varName, 'text/plain'); + }); +} + export function downloadHistoricalDataForPercentile( latlng: CsLatLong, varId: string, From 41a123e9911709f5839b92371a97c131c93fe557 Mon Sep 17 00:00:00 2001 From: MartaEY Date: Wed, 3 Dec 2025 15:32:35 +0100 Subject: [PATCH 05/28] changes to paint negative values --- anemui-core/src/BaseApp.ts | 1 + anemui-core/src/LayerManager.ts | 41 ++++++++++-- anemui-core/src/OpenLayersMap.ts | 21 ++---- anemui-core/src/PaletteManager.ts | 107 +++++++++--------------------- anemui-core/src/ui/LayerFrame.tsx | 7 ++ 5 files changed, 83 insertions(+), 94 deletions(-) diff --git a/anemui-core/src/BaseApp.ts b/anemui-core/src/BaseApp.ts index 9b74c8c..545bb0e 100644 --- a/anemui-core/src/BaseApp.ts +++ b/anemui-core/src/BaseApp.ts @@ -667,6 +667,7 @@ export abstract class BaseApp implements CsMapListener, MenuBarListener, DateFra // Continue with update even if there's an error if (!dateChanged) this.dateSelectorFrame.update(); this.paletteFrame.update(); + this.layerFrame.update(); this.changeUrl(); } } diff --git a/anemui-core/src/LayerManager.ts b/anemui-core/src/LayerManager.ts index 2042d6f..82b56aa 100644 --- a/anemui-core/src/LayerManager.ts +++ b/anemui-core/src/LayerManager.ts @@ -69,6 +69,7 @@ export class LayerManager { private topLayerVector:Layer; private topLayerImage:Image; protected uncertaintyLayer: (Image | WebGLTile)[]; + private uncertaintyLayerVisible: boolean = false; private constructor() { // CAPAS BASE @@ -98,6 +99,7 @@ export class LayerManager { this.topSelected="mapbox"; this.uncertaintyLayer = []; + this.uncertaintyLayerVisible = false; } // Base Layer @@ -248,12 +250,43 @@ export class LayerManager { return this.topLayers[this.topSelected].source } - public getUncertaintyLayer():(Image | WebGLTile)[] { - this.uncertaintyLayer = []; + public setUncertaintyLayers(layers: (Image | WebGLTile)[]): void { + this.uncertaintyLayer = layers; + this.showUncertaintyLayer(this.uncertaintyLayerVisible); + } + + + public getUncertaintyLayer(): (Image | WebGLTile)[] { return this.uncertaintyLayer; } - public showUncertaintyLayer(show: boolean) { - this.uncertaintyLayer[0].setVisible(show); + public showUncertaintyLayer(show: boolean): void { + + this.uncertaintyLayerVisible = show; + + if (!this.uncertaintyLayer || this.uncertaintyLayer.length === 0) { + console.warn('No uncertainty layers available to toggle (state saved for later)'); + return; + } + + this.uncertaintyLayer.forEach((layer, index) => { + if (layer && typeof layer.setVisible === 'function') { + layer.setVisible(show); + layer.changed(); + console.log(`${index} visibility set to:`, show); + } else { + console.error(`${index} is undefined or missing setVisible method`); + } + }); + } + + + public isUncertaintyLayerVisible(): boolean { + return this.uncertaintyLayerVisible; + } + + + public clearUncertaintyLayers(): void { + this.uncertaintyLayer = []; } } \ No newline at end of file diff --git a/anemui-core/src/OpenLayersMap.ts b/anemui-core/src/OpenLayersMap.ts index 2412248..0a93429 100644 --- a/anemui-core/src/OpenLayersMap.ts +++ b/anemui-core/src/OpenLayersMap.ts @@ -532,8 +532,8 @@ public buildUncertaintyLayer(state: CsViewerData, timesJs: CsTimesJsData): void let lmgr = LayerManager.getInstance(); let app = window.CsViewerApp; - // Safely remove existing uncertainty layers this.safelyRemoveUncertaintyLayers(); + lmgr.clearUncertaintyLayers(); const uncertaintyVarId = state.varId + '_uncertainty'; @@ -551,9 +551,9 @@ public buildUncertaintyLayer(state: CsViewerData, timesJs: CsTimesJsData): void console.log(`Creating uncertainty layer for portion: ${portion}`); let imageLayer: ImageLayer = new ImageLayer({ - visible: true, + visible: true, // Construir visible temporalmente opacity: 1.0, - zIndex: 5001 + index, // IMPORTANTE: zIndex mayor que las capas de datos + zIndex: 5001 + index, source: null, properties: { 'name': `uncertainty-layer-${index}`, @@ -563,14 +563,12 @@ public buildUncertaintyLayer(state: CsViewerData, timesJs: CsTimesJsData): void this.uncertaintyLayer.push(imageLayer); - // Insertar DESPUÉS de las capas de datos const layers = this.map.getLayers(); const dataLayerIndex = layers.getArray().findIndex(l => l.getProperties()['name']?.startsWith('data-layer') ); if (dataLayerIndex !== -1) { - // Insertar justo después de las capas de datos layers.insertAt(dataLayerIndex + 1 + index, imageLayer); } else { layers.push(imageLayer); @@ -589,24 +587,17 @@ public buildUncertaintyLayer(state: CsViewerData, timesJs: CsTimesJsData): void console.log('Building uncertainty images...'); buildImages(promises, this.uncertaintyLayer, state, timesJs, app, this.ncExtents, true) .then(() => { - console.log('✅ Uncertainty layers built successfully'); - // Asegurar visibilidad - this.uncertaintyLayer.forEach((layer, i) => { - layer.setVisible(true); - layer.changed(); - console.log(`Uncertainty layer ${i} visible:`, layer.getVisible()); - }); + console.log('✅ Uncertainty images built successfully.'); - // Forzar render + + lmgr.setUncertaintyLayers(this.uncertaintyLayer); this.map.render(); - this.map.renderSync(); }) .catch(error => { console.error('❌ Error building uncertainty images:', error); }); } } - private safelyRemoveUncertaintyLayers(): void { if (this.uncertaintyLayer && Array.isArray(this.uncertaintyLayer)) { this.uncertaintyLayer.forEach((layer: ImageLayer | TileLayer) => { diff --git a/anemui-core/src/PaletteManager.ts b/anemui-core/src/PaletteManager.ts index 1caf1c6..bd4defe 100644 --- a/anemui-core/src/PaletteManager.ts +++ b/anemui-core/src/PaletteManager.ts @@ -181,62 +181,23 @@ public async paintValues(floatArray: number[], width: number, height: number, mi height = 1; } - // 🔍 DIAGNÓSTICO 1: Calidad de datos de entrada - let dataQuality = { - total: floatArray.length, - nan: 0, - infinite: 0, - valid: 0, - samples: [] as number[] - }; - - for (let i = 0; i < floatArray.length; i++) { - const val = floatArray[i]; - if (isNaN(val)) { - dataQuality.nan++; - } else if (!isFinite(val)) { - dataQuality.infinite++; - } else { - dataQuality.valid++; - // Guardar algunos samples - if (dataQuality.samples.length < 20) { - dataQuality.samples.push(val); - } - } - } - - console.log('📊 Data Quality Check:', { - total: dataQuality.total, - valid: dataQuality.valid + ` (${(dataQuality.valid / dataQuality.total * 100).toFixed(2)}%)`, - nan: dataQuality.nan, - infinite: dataQuality.infinite, - firstSamples: dataQuality.samples.slice(0, 10) - }); - let canvas: HTMLCanvasElement = document.createElement('canvas'); let context: CanvasRenderingContext2D = canvas.getContext('2d'); canvas.width = width; canvas.height = height; let imgData: ImageData = context.getImageData(0, 0, width, height); - let gradient = PaletteManager.getInstance().updatePalete32(uncertaintyLayer); - - console.log('Gradient (colors) available:', gradient.length); - console.log('Ranges available:', this.ranges.length); - + let gradient = PaletteManager.getInstance().updatePalete32(uncertaintyLayer); const bitmap: Uint32Array = new Uint32Array(imgData.data.buffer); - // Debug stats let debugStats = { transparent: 0, - byRange: Array(this.ranges.length).fill(0), - minValue: Infinity, - maxValue: -Infinity, - indexReturned: {} as Record, // Contar qué índices retorna getValIndex - valuesOutOfRange: [] as number[] // Valores que no caen en ningún rango + painted: 0, + byValue: {} as Record, + sampleValues: [] as number[] }; - // PINTAR Y RECOPILAR ESTADÍSTICAS + for (let y: number = 0; y < height; y++) { for (let x: number = 0; x < width; x++) { let ncIndex: number = x + y * width; @@ -244,28 +205,34 @@ public async paintValues(floatArray: number[], width: number, height: number, mi let pxIndex: number = x + ((height - 1) - y) * width; if (!isNaN(value) && isFinite(value)) { - // Actualizar estadísticas - debugStats.minValue = Math.min(debugStats.minValue, value); - debugStats.maxValue = Math.max(debugStats.maxValue, value); - - let index: number = this.getValIndex(value); - - // 🔍 DIAGNÓSTICO 2: Registrar qué índices se están retornando - if (!debugStats.indexReturned[index]) { - debugStats.indexReturned[index] = 0; - } - debugStats.indexReturned[index]++; - - if (index >= 0 && index < gradient.length) { - debugStats.byRange[index]++; - bitmap[pxIndex] = gradient[index]; - } else { - debugStats.transparent++; - bitmap[pxIndex] = pxTransparent; + if (uncertaintyLayer) { + if (value > 0) { + // Hay incertidumbre, pintar gris (índice 0 de la paleta) + bitmap[pxIndex] = gradient[0]; + debugStats.painted++; + + if (debugStats.sampleValues.length < 20) { + debugStats.sampleValues.push(value); + } + } else { + // Sin incertidumbre, transparente + bitmap[pxIndex] = pxTransparent; + debugStats.transparent++; + } + + // Contar distribución de valores + const roundedVal = Math.round(value * 10) / 10; + debugStats.byValue[roundedVal] = (debugStats.byValue[roundedVal] || 0) + 1; - // 🔍 DIAGNÓSTICO 3: Guardar valores que caen fuera de rango - if (debugStats.valuesOutOfRange.length < 20) { - debugStats.valuesOutOfRange.push(value); + } else { + let index: number = this.getValIndex(value); + + if (index >= 0 && index < gradient.length) { + bitmap[pxIndex] = gradient[index]; + debugStats.painted++; + } else { + bitmap[pxIndex] = pxTransparent; + debugStats.transparent++; } } } else { @@ -275,16 +242,6 @@ public async paintValues(floatArray: number[], width: number, height: number, mi } } - // Mostrar estadísticas completas - console.log('🎨 Paint Stats:', { - dataRange: [debugStats.minValue, debugStats.maxValue], - totalPixels: width * height, - transparent: debugStats.transparent + ` (${(debugStats.transparent / (width * height) * 100).toFixed(2)}%)`, - pixelsByRange: debugStats.byRange, - indexReturned: debugStats.indexReturned, - valuesOutOfRange: debugStats.valuesOutOfRange.length > 0 ? debugStats.valuesOutOfRange : 'None' - }); - context.putImageData(imgData, 0, 0); return canvas; } diff --git a/anemui-core/src/ui/LayerFrame.tsx b/anemui-core/src/ui/LayerFrame.tsx index b863e64..d73b7da 100644 --- a/anemui-core/src/ui/LayerFrame.tsx +++ b/anemui-core/src/ui/LayerFrame.tsx @@ -249,5 +249,12 @@ export default class LayerFrame extends BaseFrame { } } } + const checkbox = document.getElementById('uncertaintySwitch') as HTMLInputElement; + if (checkbox) { + const shouldBeChecked = lmgr.isUncertaintyLayerVisible(); + if (checkbox.checked !== shouldBeChecked) { + checkbox.checked = shouldBeChecked; + } + } } } \ No newline at end of file From c2e8bddbbcf389ac51de5db1fb63889fa8e650bb Mon Sep 17 00:00:00 2001 From: ManuelPruebas Date: Thu, 4 Dec 2025 10:05:44 +0100 Subject: [PATCH 06/28] =?UTF-8?q?A=C3=B1adida=20nueva=20paleta=20de=20tram?= =?UTF-8?q?oado=20para=20incertidumbre?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- anemui-core/src/BaseApp.ts | 31 ++- anemui-core/src/OpenLayersMap.ts | 2 +- anemui-core/src/PaletteManager.ts | 256 +++++++++++++++++++++++- anemui-core/src/data/ChunkDownloader.ts | 7 +- anemui-core/src/index.ts | 2 +- anemui-core/src/ui/CsMenuItem.tsx | 2 +- 6 files changed, 290 insertions(+), 10 deletions(-) diff --git a/anemui-core/src/BaseApp.ts b/anemui-core/src/BaseApp.ts index d1b3bfe..369ce62 100644 --- a/anemui-core/src/BaseApp.ts +++ b/anemui-core/src/BaseApp.ts @@ -777,14 +777,37 @@ export abstract class BaseApp implements CsMapListener, MenuBarListener, DateFra this.csMap.refreshFeatureLayer() } + /** + * Verifica si estamos en modo climatología con ciclo temporal (mensual o estacional) + */ + protected isClimatologyCyclicMode(): boolean { + return this.state.climatology && + (this.state.timeSpan === CsTimeSpan.Month || this.state.timeSpan === CsTimeSpan.Season); + } + public dateDateBack(): void { - if (this.state.selectedTimeIndex == 0) return; - this.state.selectedTimeIndex--; + if (this.state.selectedTimeIndex == 0) { + if (this.isClimatologyCyclicMode()) { + this.state.selectedTimeIndex = this.state.times.length - 1; + } else { + return; + } + } else { + this.state.selectedTimeIndex--; + } this.update() } + public dateDateForward(): void { - if (this.state.selectedTimeIndex == this.state.times.length - 1) return; - this.state.selectedTimeIndex++; + if (this.state.selectedTimeIndex == this.state.times.length - 1) { + if (this.isClimatologyCyclicMode()) { + this.state.selectedTimeIndex = 0; + } else { + return; + } + } else { + this.state.selectedTimeIndex++; + } this.update() } diff --git a/anemui-core/src/OpenLayersMap.ts b/anemui-core/src/OpenLayersMap.ts index f1c79e0..edd40c2 100644 --- a/anemui-core/src/OpenLayersMap.ts +++ b/anemui-core/src/OpenLayersMap.ts @@ -549,7 +549,7 @@ public buildDataTilesLayers(state: CsViewerData, timesJs: CsTimesJsData): void { timesJs.portions[uncertaintyVarId].forEach((portion: string, index, array) => { let imageLayer: ImageLayer = new ImageLayer({ - visible: true, + visible: false, opacity: 1.0, zIndex: 100, source: null diff --git a/anemui-core/src/PaletteManager.ts b/anemui-core/src/PaletteManager.ts index b445e64..49897c2 100644 --- a/anemui-core/src/PaletteManager.ts +++ b/anemui-core/src/PaletteManager.ts @@ -473,10 +473,21 @@ export class PaletteManager { this.addPalette("blue", () => { return ["#FFFFFF", "#FFFFFD", "#FFFFFC", "#FFFFFA", "#FFFFF9", "#FFFFF8", "#FFFFF6", "#FFFFF5", "#FFFFF4", "#FFFFF2", "#FFFFF1", "#FFFFF0", "#FFFFEE", "#FFFFED", "#FFFFEC", "#FFFFEA", "#FFFFE9", "#FFFFE8", "#FFFFE6", "#FFFFE5", "#FFFFE4", "#FFFFE2", "#FFFFE1", "#FFFFE0", "#FFFFDE", "#FFFFDD", "#FFFFDC", "#FFFFDA", "#FFFFD9", "#FEFED8", "#FDFED6", "#FDFED5", "#FCFED3", "#FCFDD2", "#FBFDD1", "#FAFDCF", "#FAFDCE", "#F9FCCC", "#F8FCCB", "#F8FCC9", "#F7FCC8", "#F6FBC7", "#F6FBC5", "#F5FBC4", "#F5FBC2", "#F4FAC1", "#F3FAC0", "#F3FABE", "#F2FABD", "#F1F9BB", "#F1F9BA", "#F0F9B9", "#EFF9B7", "#EFF8B6", "#EEF8B4", "#EEF8B3", "#EDF8B1", "#ECF7B1", "#EBF7B1", "#E9F6B1", "#E8F6B1", "#E7F5B1", "#E5F5B1", "#E4F4B1", "#E3F4B1", "#E1F3B1", "#E0F3B1", "#DFF2B2", "#DDF2B2", "#DCF1B2", "#DBF0B2", "#D9F0B2", "#D8EFB2", "#D7EFB2", "#D5EEB2", "#D4EEB2", "#D3EDB3", "#D1EDB3", "#D0ECB3", "#CFECB3", "#CDEBB3", "#CCEBB3", "#CBEAB3", "#C9EAB3", "#C8E9B3", "#C7E9B4", "#C4E8B4", "#C1E7B4", "#BFE6B4", "#BCE5B4", "#BAE4B5", "#B7E3B5", "#B5E2B5", "#B2E1B5", "#B0E0B6", "#ADDFB6", "#ABDEB6", "#A8DDB6", "#A5DCB7", "#A3DBB7", "#A0DAB7", "#9ED9B7", "#9BD8B8", "#99D7B8", "#96D6B8", "#94D5B8", "#91D4B9", "#8FD3B9", "#8CD2B9", "#8AD1B9", "#87D0BA", "#84CFBA", "#82CEBA", "#7FCDBA", "#7DCCBB", "#7BCBBB", "#79CABB", "#76CABC", "#74C9BC", "#72C8BC", "#70C7BD", "#6EC6BD", "#6CC5BD", "#69C5BE", "#67C4BE", "#65C3BE", "#63C2BF", "#61C1BF", "#5EC1BF", "#5CC0BF", "#5ABFC0", "#58BEC0", "#56BDC0", "#53BDC1", "#51BCC1", "#4FBBC1", "#4DBAC2", "#4BB9C2", "#49B8C2", "#46B8C3", "#44B7C3", "#42B6C3", "#40B5C3", "#3FB4C3", "#3EB2C3", "#3CB1C3", "#3BB0C3", "#3AAFC3", "#38ADC3", "#37ACC2", "#36ABC2", "#35A9C2", "#33A8C2", "#32A7C2", "#31A5C2", "#30A4C2", "#2EA3C1", "#2DA1C1", "#2CA0C1", "#2A9FC1", "#299EC1", "#289CC1", "#279BC1", "#259AC0", "#2498C0", "#2397C0", "#2296C0", "#2094C0", "#1F93C0", "#1E92C0", "#1D91C0", "#1D8FBF", "#1D8DBE", "#1D8BBD", "#1D89BC", "#1D87BB", "#1E86BA", "#1E84BA", "#1E82B9", "#1E80B8", "#1E7FB7", "#1E7DB6", "#1F7BB5", "#1F79B4", "#1F77B4", "#1F75B3", "#1F74B2", "#2072B1", "#2070B0", "#206EAF", "#206CAF", "#206BAE", "#2069AD", "#2167AC", "#2165AB", "#2163AA", "#2162A9", "#2160A9", "#215EA8", "#225DA7", "#225BA6", "#225AA6", "#2258A5", "#2257A4", "#2255A3", "#2254A3", "#2252A2", "#2251A1", "#234FA1", "#234EA0", "#234C9F", "#234B9F", "#23499E", "#23489D", "#23469C", "#23459C", "#23439B", "#23429A", "#24409A", "#243F99", "#243D98", "#243C98", "#243A97", "#243996", "#243795", "#243695", "#243494", "#243393", "#233291", "#22328F", "#21318C", "#20308A", "#1F2F88", "#1E2E86", "#1D2E84", "#1C2D82", "#1B2C80", "#1A2B7E", "#192A7B", "#182979", "#172977", "#162875", "#152773", "#142671", "#13256F", "#12256D", "#11246B", "#102368", "#0F2266", "#0E2164", "#0D2162", "#0C2060", "#0B1F5E", "#0A1E5C", "#091D5A", "#081D58"]; }) + // Paleta de incertidumbre con puntos (nueva) this.addPalette("uncertainty", ()=> { return ['#65656580', '#00000000'] + }, new DotPatternPainter(1, '#656565', 0.8)) + + // Paleta de incertidumbre con tramado (alternativa con líneas) + this.addPalette("uncertainty_hatch", ()=> { + return ['#65656580', '#00000000'] + }, new HatchPatternPainter(4, 45, '#656565', 0.5)) + + // Paleta de incertidumbre sólida (backup - comportamiento antiguo) + this.addPalette("uncertainty_solid", ()=> { + return ['#65656580', '#00000000'] }) - + this.paletteBuffer = new ArrayBuffer(256 * 4); this.palette = new Uint8Array(this.paletteBuffer); this.painter = new CsDynamicPainter(); @@ -611,4 +622,247 @@ export class PaletteManager { public getUncertaintyLayerChecked():string{ return this.uncertaintyLayerChecked; } +} + +/** + * DotPatternPainter - Painter especializado para capa de incertidumbre con puntos + * Aplica un patrón de puntos centrados en cada píxel + */ +export class DotPatternPainter implements Painter { + private dotRadius: number = 1; // Radio del punto en píxeles + private dotColor: string = '#656565'; // Color del punto + private dotOpacity: number = 0.8; // Opacidad del punto + + constructor(radius: number = 1, color: string = '#656565', opacity: number = 0.8) { + this.dotRadius = radius; + this.dotColor = color; + this.dotOpacity = opacity; + } + + public async paintValues( + floatArray: number[], + width: number, + height: number, + minArray: number, + maxArray: number, + pxTransparent: number, + uncertaintyLayer: boolean + ): Promise { + // Validar dimensiones + width = Math.max(1, Math.floor(width)); + height = Math.max(1, Math.floor(height)); + + if (!isFinite(width) || !isFinite(height)) { + console.error('Invalid canvas dimensions:', width, height); + width = 1; + height = 1; + } + + let canvas: HTMLCanvasElement = document.createElement('canvas'); + let context: CanvasRenderingContext2D = canvas.getContext('2d'); + canvas.width = width; + canvas.height = height; + + // Calcular el tamaño adaptativo del punto basado en las dimensiones del canvas + // A menor resolución (menos píxeles), puntos más grandes + // A mayor resolución (más píxeles), puntos más pequeños pero visibles + const minDimension = Math.min(width, height); + let adaptiveRadius: number; + + if (minDimension < 100) { + // Rejillas muy pequeñas: puntos grandes (30-40% del píxel) + adaptiveRadius = this.dotRadius * 3; + } else if (minDimension < 300) { + // Rejillas medianas: puntos medianos (20-30% del píxel) + adaptiveRadius = this.dotRadius * 2; + } else if (minDimension < 600) { + // Rejillas grandes: puntos pequeños pero visibles + adaptiveRadius = this.dotRadius * 1.5; + } else { + // Rejillas muy grandes: mantener el tamaño base + adaptiveRadius = this.dotRadius; + } + + // Asegurar un mínimo visible + adaptiveRadius = Math.max(adaptiveRadius, 0.5); + + // Configurar estilo del punto + const rgbaMatch = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(this.dotColor); + if (rgbaMatch) { + const r = parseInt(rgbaMatch[1], 16); + const g = parseInt(rgbaMatch[2], 16); + const b = parseInt(rgbaMatch[3], 16); + context.fillStyle = `rgba(${r},${g},${b},${this.dotOpacity})`; + } else { + context.fillStyle = this.dotColor; + } + + // Dibujar puntos centrados en cada píxel con datos válidos + for (let y: number = 0; y < height; y++) { + for (let x: number = 0; x < width; x++) { + let ncIndex: number = x + y * width; + let value: number = floatArray[ncIndex]; + + // Si el valor es válido (no NaN y finito), dibujar punto + if (!isNaN(value) && isFinite(value)) { + // Invertir Y para mantener consistencia con otros painters + let canvasY = (height - 1) - y; + + // Dibujar punto centrado en el píxel con tamaño adaptativo + context.beginPath(); + context.arc(x + 0.5, canvasY + 0.5, adaptiveRadius, 0, 2 * Math.PI); + context.fill(); + } + } + } + + return canvas; + } + + public getColorString(val: number, min: number, max: number): string { + // Para compatibilidad con la interfaz Painter + return this.dotColor; + } + + public getValIndex(val: number): number { + // Para compatibilidad con la interfaz Painter + return 0; + } +} + +/** + * HatchPatternPainter - Painter especializado para capa de incertidumbre + * Aplica un patrón de líneas diagonales que se adapta al nivel de zoom + */ +export class HatchPatternPainter implements Painter { + private hatchSpacing: number = 4; // Espaciado base entre líneas + private hatchAngle: number = 45; // Ángulo del tramado en grados + private hatchColor: string = '#656565'; // Color del tramado + private hatchOpacity: number = 0.5; // Opacidad del tramado + + constructor(spacing: number = 4, angle: number = 45, color: string = '#656565', opacity: number = 0.5) { + this.hatchSpacing = spacing; + this.hatchAngle = angle; + this.hatchColor = color; + this.hatchOpacity = opacity; + } + + public async paintValues( + floatArray: number[], + width: number, + height: number, + minArray: number, + maxArray: number, + pxTransparent: number, + uncertaintyLayer: boolean + ): Promise { + // Validar dimensiones + width = Math.max(1, Math.floor(width)); + height = Math.max(1, Math.floor(height)); + + if (!isFinite(width) || !isFinite(height)) { + console.error('Invalid canvas dimensions:', width, height); + width = 1; + height = 1; + } + + let canvas: HTMLCanvasElement = document.createElement('canvas'); + let context: CanvasRenderingContext2D = canvas.getContext('2d'); + canvas.width = width; + canvas.height = height; + + // Crear una máscara con los datos válidos + let maskCanvas: HTMLCanvasElement = document.createElement('canvas'); + let maskContext: CanvasRenderingContext2D = maskCanvas.getContext('2d'); + maskCanvas.width = width; + maskCanvas.height = height; + + let maskData: ImageData = maskContext.getImageData(0, 0, width, height); + let maskBitmap: Uint32Array = new Uint32Array(maskData.data.buffer); + + // Crear máscara: blanco donde hay datos válidos, transparente donde no + for (let y: number = 0; y < height; y++) { + for (let x: number = 0; x < width; x++) { + let ncIndex: number = x + y * width; + let value: number = floatArray[ncIndex]; + let pxIndex: number = x + ((height - 1) - y) * width; + + // Si el valor es válido (no NaN y finito), marcarlo en la máscara + if (!isNaN(value) && isFinite(value)) { + maskBitmap[pxIndex] = 0xFFFFFFFF; // Blanco opaco + } else { + maskBitmap[pxIndex] = 0x00000000; // Transparente + } + } + } + + maskContext.putImageData(maskData, 0, 0); + + // Ajustar el espaciado según el tamaño del canvas (más detalle a mayor zoom) + // A mayor tamaño de canvas, mayor zoom, más espaciado entre líneas + let adaptiveSpacing = Math.max(2, Math.floor(this.hatchSpacing * (width / 500))); + + // Dibujar el patrón de tramado + context.strokeStyle = this.hatchColor; + context.globalAlpha = this.hatchOpacity; + context.lineWidth = 1; + + // Guardar el contexto para restaurar después + context.save(); + + // Aplicar la máscara usando globalCompositeOperation + // Primero dibujamos el patrón, luego aplicamos la máscara + + // Dibujar líneas diagonales + const angleRad = (this.hatchAngle * Math.PI) / 180; + const diagonal = Math.sqrt(width * width + height * height); + + context.beginPath(); + + // Calcular número de líneas necesarias + const numLines = Math.ceil(diagonal / adaptiveSpacing); + + for (let i = -numLines; i <= numLines; i++) { + const offset = i * adaptiveSpacing; + + // Calcular puntos de inicio y fin de la línea diagonal + const x1 = -diagonal; + const y1 = offset; + const x2 = diagonal; + const y2 = offset; + + // Rotar y trasladar + const cos = Math.cos(angleRad); + const sin = Math.sin(angleRad); + + const rx1 = x1 * cos - y1 * sin + width / 2; + const ry1 = x1 * sin + y1 * cos + height / 2; + const rx2 = x2 * cos - y2 * sin + width / 2; + const ry2 = x2 * sin + y2 * cos + height / 2; + + context.moveTo(rx1, ry1); + context.lineTo(rx2, ry2); + } + + context.stroke(); + + // Aplicar la máscara: solo mantener el tramado donde hay datos válidos + context.globalCompositeOperation = 'destination-in'; + context.globalAlpha = 1.0; + context.drawImage(maskCanvas, 0, 0); + + context.restore(); + + return canvas; + } + + public getColorString(val: number, min: number, max: number): string { + // Para compatibilidad con la interfaz Painter + return this.hatchColor; + } + + public getValIndex(val: number): number { + // Para compatibilidad con la interfaz Painter + return 0; + } } \ No newline at end of file diff --git a/anemui-core/src/data/ChunkDownloader.ts b/anemui-core/src/data/ChunkDownloader.ts index 9fe344e..3969b0d 100644 --- a/anemui-core/src/data/ChunkDownloader.ts +++ b/anemui-core/src/data/ChunkDownloader.ts @@ -362,7 +362,10 @@ export async function buildImages(promises: Promise[], dataTilesLayer: app.notifyMaxMinChanged(); - let painterInstance = PaletteManager.getInstance().getPainter(); + // Si es capa de incertidumbre, usar el painter específico de uncertainty + let painterInstance = uncertaintyLayer + ? PaletteManager.getInstance()['painters']['uncertainty'] || PaletteManager.getInstance().getPainter() + : PaletteManager.getInstance().getPainter(); // Para datos computados, precalcular breaks con todos los datos combinados if (status.computedLayer && allValidNumbers.length > 0 && (painterInstance as any).setPrecalculatedBreaks) { @@ -394,7 +397,7 @@ export async function buildImages(promises: Promise[], dataTilesLayer: // zIndex menor que 5000 para que los labels queden por encima dataTilesLayer[i].setZIndex(4000 + i); - dataTilesLayer[i].setVisible(true); + dataTilesLayer[i].setVisible(uncertaintyLayer ? false : true); dataTilesLayer[i].setOpacity(1.0); dataTilesLayer[i].changed(); diff --git a/anemui-core/src/index.ts b/anemui-core/src/index.ts index e077c36..0a58427 100644 --- a/anemui-core/src/index.ts +++ b/anemui-core/src/index.ts @@ -16,7 +16,7 @@ export {enableRenderer } from './tiles/Support'; // public APIs export { CsGraph, type GraphType } from './ui/Graph'; -export { MenuBar, MenuBarListener } from './ui/MenuBar'; +export { MenuBar, MenuBarListener, simpleDiv } from './ui/MenuBar'; export { DateFrameMode } from "./ui/DateFrame"; export { fromLonLat } from 'ol/proj'; diff --git a/anemui-core/src/ui/CsMenuItem.tsx b/anemui-core/src/ui/CsMenuItem.tsx index d8028e8..a531921 100644 --- a/anemui-core/src/ui/CsMenuItem.tsx +++ b/anemui-core/src/ui/CsMenuItem.tsx @@ -86,7 +86,7 @@ export class CsMenuItem extends BaseUiElement { public render(_subTitle?: string, hasPopData: boolean = false): JSX.Element { let count = 0; for (let i = 0; i < this.values.length; i++) { - if (!this.values[i].startsWith("-")) { + if (!this.values[i].startsWith("~") && !this.values[i].startsWith("-")) { count++; } } From 1d7f4eeeeae9207b2308dca25db040b27c3c8bf2 Mon Sep 17 00:00:00 2001 From: ManuelPruebas Date: Fri, 5 Dec 2025 13:27:15 +0100 Subject: [PATCH 07/28] Cambios AnemUI - 05/12/2025 --- anemui-core/css/open-layers-map.scss | 7 ++ anemui-core/css/sidebar.scss | 12 ++-- anemui-core/css/topbar.scss | 10 ++- anemui-core/src/OpenLayersMap.ts | 48 +++++++++++-- anemui-core/src/PaletteManager.ts | 89 +++++++++++++------------ anemui-core/src/data/ChunkDownloader.ts | 5 +- anemui-core/src/index.ts | 2 +- anemui-core/src/ui/DateFrame.tsx | 6 +- 8 files changed, 119 insertions(+), 60 deletions(-) diff --git a/anemui-core/css/open-layers-map.scss b/anemui-core/css/open-layers-map.scss index 675b019..c6b0158 100644 --- a/anemui-core/css/open-layers-map.scss +++ b/anemui-core/css/open-layers-map.scss @@ -1,5 +1,12 @@ @import "~ol/ol.css"; +// Desactivar suavizado solo para la capa de incertidumbre +.uncertainty-layer canvas { + image-rendering: pixelated; + image-rendering: -moz-crisp-edges; + image-rendering: crisp-edges; +} + // .marker { // width: 20px; // height: 20px; diff --git a/anemui-core/css/sidebar.scss b/anemui-core/css/sidebar.scss index 1b24a93..523170f 100644 --- a/anemui-core/css/sidebar.scss +++ b/anemui-core/css/sidebar.scss @@ -580,16 +580,15 @@ ul .two-columns { /* --------------------------------------------------- SELECTORES DE CAPAS ----------------------------------------------------- */ - div.row.selectDiv{ - width: 260px; - margin: 0.45rem 0rem; + width: 240px; + margin: 0.45rem 0rem; } .uncDiv input.form-check-input { visibility: visible; margin: 0px auto; - width: 4rem !important; + width: 4rem !important; height: 2rem !important; background-color: $color_8; box-shadow: 0.5px 0.5px 3px $color_3; @@ -632,6 +631,11 @@ div.slider.visible { display: none; } +.uncDiv .inputDiv { + background-color: transparent !important; + box-shadow: none !important; +} + .closeDiv .icon { height: 30px; width: 30px; diff --git a/anemui-core/css/topbar.scss b/anemui-core/css/topbar.scss index 7642f12..94e2b0d 100644 --- a/anemui-core/css/topbar.scss +++ b/anemui-core/css/topbar.scss @@ -187,16 +187,22 @@ @include desktop { width: 45%; #title { - font-size: 1.2em; + font-size: 0.95em; padding: 0.7rem 1rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } } @include md-desktop { width: 40%; #title { - font-size: 1.5em; + font-size: 1.1em; font-weight: 600; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } } } diff --git a/anemui-core/src/OpenLayersMap.ts b/anemui-core/src/OpenLayersMap.ts index edd40c2..dd37a1d 100644 --- a/anemui-core/src/OpenLayersMap.ts +++ b/anemui-core/src/OpenLayersMap.ts @@ -118,14 +118,14 @@ protected setExtents(timesJs: CsTimesJsData, varId: string): void { timesJs.portions[varId].forEach((portion: string, index, array) => { let selector = varId + portion; let pxSize: number = (timesJs.lonMax[selector] - timesJs.lonMin[selector]) / (timesJs.lonNum[selector] - 1); - + const dataExtent = [ - timesJs.lonMin[selector] - pxSize / 2, - timesJs.latMin[selector] - pxSize / 2, - timesJs.lonMax[selector] + pxSize / 2, + timesJs.lonMin[selector] - pxSize / 2, + timesJs.latMin[selector] - pxSize / 2, + timesJs.lonMax[selector] + pxSize / 2, timesJs.latMax[selector] + pxSize / 2 ]; - + // Si el mapa está en una proyección diferente, transformar if (timesJs.projection !== olProjection) { const transformedExtent = transformExtent( @@ -140,6 +140,38 @@ protected setExtents(timesJs: CsTimesJsData, varId: string): void { }); } +/** + * Ajusta la vista inicial del mapa para mostrar todo el territorio español + * (Península, Baleares y Canarias) adaptándose al tamaño de la pantalla + */ +protected fitInitialView(timesJs: CsTimesJsData, varId: string): void { + // Calcular el extent combinado de todas las porciones + let combinedExtent: [number, number, number, number] | null = null; + + timesJs.portions[varId].forEach((portion: string) => { + const extent = this.ncExtents[portion]; + if (extent) { + if (!combinedExtent) { + combinedExtent = [...extent] as [number, number, number, number]; + } else { + // Expandir el extent para incluir esta porción + combinedExtent[0] = Math.min(combinedExtent[0], extent[0]); // minX + combinedExtent[1] = Math.min(combinedExtent[1], extent[1]); // minY + combinedExtent[2] = Math.max(combinedExtent[2], extent[2]); // maxX + combinedExtent[3] = Math.max(combinedExtent[3], extent[3]); // maxY + } + } + }); + + // Ajustar la vista para mostrar el extent combinado + if (combinedExtent && this.map) { + this.map.getView().fit(combinedExtent, { + padding: [50, 50, 50, 50], // Padding en píxeles alrededor del extent + duration: 0 // Sin animación en la carga inicial + }); + } +} + init(_parent: CsMap): void { this.parent = _parent; @@ -162,6 +194,9 @@ protected setExtents(timesJs: CsTimesJsData, varId: string): void { }; this.map = new Map(options); + + // Ajustar la vista inicial para mostrar todo el territorio español + this.fitInitialView(timesJs, state.varId); let self = this; this.map.on('movestart', event => { self.onDragStart(event) }) this.map.on('loadend', () => { self.onMapLoaded() }) @@ -552,7 +587,8 @@ public buildDataTilesLayers(state: CsViewerData, timesJs: CsTimesJsData): void { visible: false, opacity: 1.0, zIndex: 100, - source: null + source: null, + className: 'uncertainty-layer' // Agregar clase CSS para la capa de incertidumbre }); if (imageLayer) { diff --git a/anemui-core/src/PaletteManager.ts b/anemui-core/src/PaletteManager.ts index 49897c2..64ffe0a 100644 --- a/anemui-core/src/PaletteManager.ts +++ b/anemui-core/src/PaletteManager.ts @@ -15,7 +15,7 @@ type CS_RGBA_Info = { } export interface Painter{ - paintValues(floatArray:number[],width:number,height:number,minArray:number,maxArray:number,pxTransparent:number,uncertaintyLayer:boolean):Promise + paintValues(floatArray:number[],width:number,height:number,minArray:number,maxArray:number,pxTransparent:number,uncertaintyLayer:boolean,zoom?:number):Promise getColorString(val: number, min: number, max: number): string getValIndex(val:number):number } @@ -473,18 +473,14 @@ export class PaletteManager { this.addPalette("blue", () => { return ["#FFFFFF", "#FFFFFD", "#FFFFFC", "#FFFFFA", "#FFFFF9", "#FFFFF8", "#FFFFF6", "#FFFFF5", "#FFFFF4", "#FFFFF2", "#FFFFF1", "#FFFFF0", "#FFFFEE", "#FFFFED", "#FFFFEC", "#FFFFEA", "#FFFFE9", "#FFFFE8", "#FFFFE6", "#FFFFE5", "#FFFFE4", "#FFFFE2", "#FFFFE1", "#FFFFE0", "#FFFFDE", "#FFFFDD", "#FFFFDC", "#FFFFDA", "#FFFFD9", "#FEFED8", "#FDFED6", "#FDFED5", "#FCFED3", "#FCFDD2", "#FBFDD1", "#FAFDCF", "#FAFDCE", "#F9FCCC", "#F8FCCB", "#F8FCC9", "#F7FCC8", "#F6FBC7", "#F6FBC5", "#F5FBC4", "#F5FBC2", "#F4FAC1", "#F3FAC0", "#F3FABE", "#F2FABD", "#F1F9BB", "#F1F9BA", "#F0F9B9", "#EFF9B7", "#EFF8B6", "#EEF8B4", "#EEF8B3", "#EDF8B1", "#ECF7B1", "#EBF7B1", "#E9F6B1", "#E8F6B1", "#E7F5B1", "#E5F5B1", "#E4F4B1", "#E3F4B1", "#E1F3B1", "#E0F3B1", "#DFF2B2", "#DDF2B2", "#DCF1B2", "#DBF0B2", "#D9F0B2", "#D8EFB2", "#D7EFB2", "#D5EEB2", "#D4EEB2", "#D3EDB3", "#D1EDB3", "#D0ECB3", "#CFECB3", "#CDEBB3", "#CCEBB3", "#CBEAB3", "#C9EAB3", "#C8E9B3", "#C7E9B4", "#C4E8B4", "#C1E7B4", "#BFE6B4", "#BCE5B4", "#BAE4B5", "#B7E3B5", "#B5E2B5", "#B2E1B5", "#B0E0B6", "#ADDFB6", "#ABDEB6", "#A8DDB6", "#A5DCB7", "#A3DBB7", "#A0DAB7", "#9ED9B7", "#9BD8B8", "#99D7B8", "#96D6B8", "#94D5B8", "#91D4B9", "#8FD3B9", "#8CD2B9", "#8AD1B9", "#87D0BA", "#84CFBA", "#82CEBA", "#7FCDBA", "#7DCCBB", "#7BCBBB", "#79CABB", "#76CABC", "#74C9BC", "#72C8BC", "#70C7BD", "#6EC6BD", "#6CC5BD", "#69C5BE", "#67C4BE", "#65C3BE", "#63C2BF", "#61C1BF", "#5EC1BF", "#5CC0BF", "#5ABFC0", "#58BEC0", "#56BDC0", "#53BDC1", "#51BCC1", "#4FBBC1", "#4DBAC2", "#4BB9C2", "#49B8C2", "#46B8C3", "#44B7C3", "#42B6C3", "#40B5C3", "#3FB4C3", "#3EB2C3", "#3CB1C3", "#3BB0C3", "#3AAFC3", "#38ADC3", "#37ACC2", "#36ABC2", "#35A9C2", "#33A8C2", "#32A7C2", "#31A5C2", "#30A4C2", "#2EA3C1", "#2DA1C1", "#2CA0C1", "#2A9FC1", "#299EC1", "#289CC1", "#279BC1", "#259AC0", "#2498C0", "#2397C0", "#2296C0", "#2094C0", "#1F93C0", "#1E92C0", "#1D91C0", "#1D8FBF", "#1D8DBE", "#1D8BBD", "#1D89BC", "#1D87BB", "#1E86BA", "#1E84BA", "#1E82B9", "#1E80B8", "#1E7FB7", "#1E7DB6", "#1F7BB5", "#1F79B4", "#1F77B4", "#1F75B3", "#1F74B2", "#2072B1", "#2070B0", "#206EAF", "#206CAF", "#206BAE", "#2069AD", "#2167AC", "#2165AB", "#2163AA", "#2162A9", "#2160A9", "#215EA8", "#225DA7", "#225BA6", "#225AA6", "#2258A5", "#2257A4", "#2255A3", "#2254A3", "#2252A2", "#2251A1", "#234FA1", "#234EA0", "#234C9F", "#234B9F", "#23499E", "#23489D", "#23469C", "#23459C", "#23439B", "#23429A", "#24409A", "#243F99", "#243D98", "#243C98", "#243A97", "#243996", "#243795", "#243695", "#243494", "#243393", "#233291", "#22328F", "#21318C", "#20308A", "#1F2F88", "#1E2E86", "#1D2E84", "#1C2D82", "#1B2C80", "#1A2B7E", "#192A7B", "#182979", "#172977", "#162875", "#152773", "#142671", "#13256F", "#12256D", "#11246B", "#102368", "#0F2266", "#0E2164", "#0D2162", "#0C2060", "#0B1F5E", "#0A1E5C", "#091D5A", "#081D58"]; }) - // Paleta de incertidumbre con puntos (nueva) - this.addPalette("uncertainty", ()=> { - return ['#65656580', '#00000000'] - }, new DotPatternPainter(1, '#656565', 0.8)) - // Paleta de incertidumbre con tramado (alternativa con líneas) - this.addPalette("uncertainty_hatch", ()=> { + // Paleta de incertidumbre con puntos (alternativa) + this.addPalette("uncertainty_dots", ()=> { return ['#65656580', '#00000000'] - }, new HatchPatternPainter(4, 45, '#656565', 0.5)) + }, new DotPatternPainter(1.5, '#000000', 1.0)) // Paleta de incertidumbre sólida (backup - comportamiento antiguo) - this.addPalette("uncertainty_solid", ()=> { + this.addPalette("uncertainty", ()=> { return ['#65656580', '#00000000'] }) @@ -637,6 +633,7 @@ export class DotPatternPainter implements Painter { this.dotRadius = radius; this.dotColor = color; this.dotOpacity = opacity; + console.log('🟢 DotPatternPainter creado con:', { radius, color, opacity }); } public async paintValues( @@ -646,8 +643,11 @@ export class DotPatternPainter implements Painter { minArray: number, maxArray: number, pxTransparent: number, - uncertaintyLayer: boolean + uncertaintyLayer: boolean, + zoom?: number ): Promise { + console.log('🔵 DotPatternPainter.paintValues ejecutándose:', { width, height, uncertaintyLayer, dataLength: floatArray.length }); + // Validar dimensiones width = Math.max(1, Math.floor(width)); height = Math.max(1, Math.floor(height)); @@ -663,28 +663,12 @@ export class DotPatternPainter implements Painter { canvas.width = width; canvas.height = height; - // Calcular el tamaño adaptativo del punto basado en las dimensiones del canvas - // A menor resolución (menos píxeles), puntos más grandes - // A mayor resolución (más píxeles), puntos más pequeños pero visibles - const minDimension = Math.min(width, height); - let adaptiveRadius: number; - - if (minDimension < 100) { - // Rejillas muy pequeñas: puntos grandes (30-40% del píxel) - adaptiveRadius = this.dotRadius * 3; - } else if (minDimension < 300) { - // Rejillas medianas: puntos medianos (20-30% del píxel) - adaptiveRadius = this.dotRadius * 2; - } else if (minDimension < 600) { - // Rejillas grandes: puntos pequeños pero visibles - adaptiveRadius = this.dotRadius * 1.5; - } else { - // Rejillas muy grandes: mantener el tamaño base - adaptiveRadius = this.dotRadius; - } + // Desactivar el suavizado de imágenes para mantener píxeles nítidos + context.imageSmoothingEnabled = false; - // Asegurar un mínimo visible - adaptiveRadius = Math.max(adaptiveRadius, 0.5); + // Calcular el tamaño del punto como porcentaje del píxel + // Usar puntos pequeños pero visibles (25-30% del píxel) + const dotSize = 0.3; // 30% del píxel // Configurar estilo del punto const rgbaMatch = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(this.dotColor); @@ -697,7 +681,8 @@ export class DotPatternPainter implements Painter { context.fillStyle = this.dotColor; } - // Dibujar puntos centrados en cada píxel con datos válidos + // Dibujar puntos como pequeños rectángulos/círculos centrados + let dotsDrawn = 0; for (let y: number = 0; y < height; y++) { for (let x: number = 0; x < width; x++) { let ncIndex: number = x + y * width; @@ -708,14 +693,28 @@ export class DotPatternPainter implements Painter { // Invertir Y para mantener consistencia con otros painters let canvasY = (height - 1) - y; - // Dibujar punto centrado en el píxel con tamaño adaptativo - context.beginPath(); - context.arc(x + 0.5, canvasY + 0.5, adaptiveRadius, 0, 2 * Math.PI); - context.fill(); + // Calcular la posición del punto centrado en el píxel + // El punto será un pequeño rectángulo de dotSize x dotSize + const pointX = x + (1 - dotSize) / 2; + const pointY = canvasY + (1 - dotSize) / 2; + + // Dibujar punto como rectángulo pequeño + context.fillRect(pointX, pointY, dotSize, dotSize); + dotsDrawn++; } } } + console.log('✅ DotPatternPainter terminado:', { dotsDrawn, dotSize, totalPixels: width * height }); + + // Debug: Verificar que el canvas tiene contenido + const imageData = context.getImageData(0, 0, width, height); + let nonTransparentPixels = 0; + for (let i = 3; i < imageData.data.length; i += 4) { + if (imageData.data[i] > 0) nonTransparentPixels++; + } + console.log('🎨 Píxeles no transparentes en canvas:', nonTransparentPixels); + return canvas; } @@ -737,10 +736,10 @@ export class DotPatternPainter implements Painter { export class HatchPatternPainter implements Painter { private hatchSpacing: number = 4; // Espaciado base entre líneas private hatchAngle: number = 45; // Ángulo del tramado en grados - private hatchColor: string = '#656565'; // Color del tramado - private hatchOpacity: number = 0.5; // Opacidad del tramado + private hatchColor: string = '#000'; // Color del tramado + private hatchOpacity: number = 0.2; // Opacidad del tramado - constructor(spacing: number = 4, angle: number = 45, color: string = '#656565', opacity: number = 0.5) { + constructor(spacing: number = 4, angle: number = 45, color: string = '#000', opacity: number = 0.5) { this.hatchSpacing = spacing; this.hatchAngle = angle; this.hatchColor = color; @@ -754,7 +753,8 @@ export class HatchPatternPainter implements Painter { minArray: number, maxArray: number, pxTransparent: number, - uncertaintyLayer: boolean + uncertaintyLayer: boolean, + zoom?: number ): Promise { // Validar dimensiones width = Math.max(1, Math.floor(width)); @@ -798,9 +798,12 @@ export class HatchPatternPainter implements Painter { maskContext.putImageData(maskData, 0, 0); - // Ajustar el espaciado según el tamaño del canvas (más detalle a mayor zoom) - // A mayor tamaño de canvas, mayor zoom, más espaciado entre líneas - let adaptiveSpacing = Math.max(2, Math.floor(this.hatchSpacing * (width / 500))); + // Ajustar el espaciado según el nivel de zoom del mapa + // A mayor zoom, menor espaciado (líneas más juntas) + // Zoom típico: 5-11, donde 6 es el inicial + const zoomLevel = zoom || 6; + const zoomFactor = Math.pow(2, zoomLevel - 6); // Factor exponencial basado en nivel de zoom + let adaptiveSpacing = Math.max(1, Math.floor(this.hatchSpacing / zoomFactor)); // Dibujar el patrón de tramado context.strokeStyle = this.hatchColor; diff --git a/anemui-core/src/data/ChunkDownloader.ts b/anemui-core/src/data/ChunkDownloader.ts index 3969b0d..60455a0 100644 --- a/anemui-core/src/data/ChunkDownloader.ts +++ b/anemui-core/src/data/ChunkDownloader.ts @@ -372,6 +372,9 @@ export async function buildImages(promises: Promise[], dataTilesLayer: (painterInstance as any).setPrecalculatedBreaks(allValidNumbers); } + // Obtener el nivel de zoom actual del mapa + const currentZoom = app.getMap()?.getZoom() || 6; + for (let i = 0; i < filteredArrays.length; i++) { const filteredArray = filteredArrays[i]; @@ -380,7 +383,7 @@ export async function buildImages(promises: Promise[], dataTilesLayer: let canvas: HTMLCanvasElement | null = null; try { - canvas = await painterInstance.paintValues(filteredArray, width, height, minArray, maxArray, pxTransparent, uncertaintyLayer); + canvas = await painterInstance.paintValues(filteredArray, width, height, minArray, maxArray, pxTransparent, uncertaintyLayer, currentZoom); if (canvas) { const extent = ncExtents[timesJs.portions[status.varId][i]]; diff --git a/anemui-core/src/index.ts b/anemui-core/src/index.ts index 0a58427..d94e4b3 100644 --- a/anemui-core/src/index.ts +++ b/anemui-core/src/index.ts @@ -17,7 +17,7 @@ export {enableRenderer } from './tiles/Support'; // public APIs export { CsGraph, type GraphType } from './ui/Graph'; export { MenuBar, MenuBarListener, simpleDiv } from './ui/MenuBar'; -export { DateFrameMode } from "./ui/DateFrame"; +export { DateSelectorFrame, DateFrameListener, DateFrameMode } from "./ui/DateFrame"; export { fromLonLat } from 'ol/proj'; diff --git a/anemui-core/src/ui/DateFrame.tsx b/anemui-core/src/ui/DateFrame.tsx index 219321f..034af1b 100644 --- a/anemui-core/src/ui/DateFrame.tsx +++ b/anemui-core/src/ui/DateFrame.tsx @@ -44,9 +44,9 @@ export class DateSelectorFrame extends BaseFrame { protected datepicker: JQuery protected seasonButton: HTMLElement; protected monthButton: HTMLElement; - private climatologyFrame: HTMLElement - private climTitle: HTMLElement - private timeSeriesFrame: HTMLElement + protected climatologyFrame: HTMLElement + protected climTitle: HTMLElement + protected timeSeriesFrame: HTMLElement protected dateIndex: dateHashMap; protected monthIndex: monthHashMap; protected seasonIndex: monthHashMap; From 4471619ee2ffe5c17c7f5884b67f72d0d61f940d Mon Sep 17 00:00:00 2001 From: ManuelPruebas Date: Tue, 9 Dec 2025 12:24:25 +0100 Subject: [PATCH 08/28] Mejora tramado paleta incertidumbre: nueva clase DotPatternPainter --- anemui-core/src/OpenLayersMap.ts | 11 +++- anemui-core/src/PaletteManager.ts | 94 ++++++++++++++++++++++--------- 2 files changed, 78 insertions(+), 27 deletions(-) diff --git a/anemui-core/src/OpenLayersMap.ts b/anemui-core/src/OpenLayersMap.ts index dd37a1d..b0695f7 100644 --- a/anemui-core/src/OpenLayersMap.ts +++ b/anemui-core/src/OpenLayersMap.ts @@ -468,12 +468,21 @@ private shouldShowPercentileClock(state: CsViewerData): boolean { public buildDataTilesLayers(state: CsViewerData, timesJs: CsTimesJsData): void { let app = window.CsViewerApp; - + this.safelyRemoveDataLayers(); this.dataTilesLayer = []; + // Si la capa de incertidumbre está activa, reconstruirla con el nuevo varId + if (state.uncertaintyLayer) { + this.safelyRemoveUncertaintyLayers(); + this.buildUncertaintyLayer(state, timesJs); + // Mostrar las capas después de construirlas + const lmgr = LayerManager.getInstance(); + lmgr.showUncertaintyLayer(true); + } + if (!timesJs.portions[state.varId]) { console.warn('No portions found for varId:', state.varId); return; diff --git a/anemui-core/src/PaletteManager.ts b/anemui-core/src/PaletteManager.ts index 64ffe0a..6a8a5ba 100644 --- a/anemui-core/src/PaletteManager.ts +++ b/anemui-core/src/PaletteManager.ts @@ -628,12 +628,14 @@ export class DotPatternPainter implements Painter { private dotRadius: number = 1; // Radio del punto en píxeles private dotColor: string = '#656565'; // Color del punto private dotOpacity: number = 0.8; // Opacidad del punto + private dotSpacing: number = 3; // Espaciado entre puntos (cada N píxeles) - constructor(radius: number = 1, color: string = '#656565', opacity: number = 0.8) { + constructor(radius: number = 1, color: string = '#656565', opacity: number = 0.8, spacing: number = 3) { this.dotRadius = radius; this.dotColor = color; this.dotOpacity = opacity; - console.log('🟢 DotPatternPainter creado con:', { radius, color, opacity }); + this.dotSpacing = spacing; + console.log('🟢 DotPatternPainter creado con:', { radius, color, opacity, spacing }); } public async paintValues( @@ -666,52 +668,92 @@ export class DotPatternPainter implements Painter { // Desactivar el suavizado de imágenes para mantener píxeles nítidos context.imageSmoothingEnabled = false; - // Calcular el tamaño del punto como porcentaje del píxel - // Usar puntos pequeños pero visibles (25-30% del píxel) - const dotSize = 0.3; // 30% del píxel - - // Configurar estilo del punto + // Parsear el color a valores RGB const rgbaMatch = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(this.dotColor); + let r = 101, g = 101, b = 101; // Valores por defecto para #656565 if (rgbaMatch) { - const r = parseInt(rgbaMatch[1], 16); - const g = parseInt(rgbaMatch[2], 16); - const b = parseInt(rgbaMatch[3], 16); - context.fillStyle = `rgba(${r},${g},${b},${this.dotOpacity})`; - } else { - context.fillStyle = this.dotColor; + r = parseInt(rgbaMatch[1], 16); + g = parseInt(rgbaMatch[2], 16); + b = parseInt(rgbaMatch[3], 16); } - // Dibujar puntos como pequeños rectángulos/círculos centrados + // Convertir opacidad (0-1) a alpha (0-255) + const alpha = Math.round(this.dotOpacity * 255); + + // Crear ImageData para manipular píxeles directamente + const imageData = context.createImageData(width, height); + const data = imageData.data; + + // Analizar los valores para debug + let validValues = 0; + let zeroValues = 0; + let nanValues = 0; + let minVal = Infinity; + let maxVal = -Infinity; + let sampleValues: number[] = []; + + for (let i = 0; i < Math.min(floatArray.length, 100); i++) { + const val = floatArray[i]; + if (!isNaN(val) && isFinite(val)) { + validValues++; + if (val === 0) zeroValues++; + if (val < minVal) minVal = val; + if (val > maxVal) maxVal = val; + if (sampleValues.length < 10) sampleValues.push(val); + } else { + nanValues++; + } + } + + console.log('📊 Análisis de valores (primeros 100):', { + validValues, zeroValues, nanValues, minVal, maxVal, sampleValues + }); + + // Pintar puntos cuadrados con espaciado solo donde hay incertidumbre let dotsDrawn = 0; + let skippedDots = 0; for (let y: number = 0; y < height; y++) { for (let x: number = 0; x < width; x++) { + // Aplicar patrón de espaciado: solo pintar cada N píxeles + if ((x % this.dotSpacing !== 0) || (y % this.dotSpacing !== 0)) { + continue; + } + let ncIndex: number = x + y * width; let value: number = floatArray[ncIndex]; - // Si el valor es válido (no NaN y finito), dibujar punto - if (!isNaN(value) && isFinite(value)) { + // Solo pintar si el valor indica incertidumbre (valor > 0) + if (!isNaN(value) && isFinite(value) && value > 0) { // Invertir Y para mantener consistencia con otros painters let canvasY = (height - 1) - y; - // Calcular la posición del punto centrado en el píxel - // El punto será un pequeño rectángulo de dotSize x dotSize - const pointX = x + (1 - dotSize) / 2; - const pointY = canvasY + (1 - dotSize) / 2; + // Calcular índice en el array de ImageData + let pixelIndex = (canvasY * width + x) * 4; + + // Establecer valores RGBA (píxel cuadrado) + data[pixelIndex] = r; // Red + data[pixelIndex + 1] = g; // Green + data[pixelIndex + 2] = b; // Blue + data[pixelIndex + 3] = alpha; // Alpha - // Dibujar punto como rectángulo pequeño - context.fillRect(pointX, pointY, dotSize, dotSize); dotsDrawn++; + } else if (!isNaN(value) && isFinite(value)) { + skippedDots++; } } } - console.log('✅ DotPatternPainter terminado:', { dotsDrawn, dotSize, totalPixels: width * height }); + console.log('🎯 Puntos dibujados vs saltados:', { dotsDrawn, skippedDots }); + + // Escribir los datos de píxeles al canvas + context.putImageData(imageData, 0, 0); + + console.log('✅ DotPatternPainter terminado:', { dotsDrawn, totalPixels: width * height, alpha }); // Debug: Verificar que el canvas tiene contenido - const imageData = context.getImageData(0, 0, width, height); let nonTransparentPixels = 0; - for (let i = 3; i < imageData.data.length; i += 4) { - if (imageData.data[i] > 0) nonTransparentPixels++; + for (let i = 3; i < data.length; i += 4) { + if (data[i] > 0) nonTransparentPixels++; } console.log('🎨 Píxeles no transparentes en canvas:', nonTransparentPixels); From 8d4542a3aac71b4f1d32d261d2a4ced1682b0d4a Mon Sep 17 00:00:00 2001 From: ManuelPruebas Date: Wed, 10 Dec 2025 15:05:10 +0100 Subject: [PATCH 09/28] =?UTF-8?q?Arreglado=20error=20en=20la=20l=C3=B3gica?= =?UTF-8?q?=20de=20CsmenuItem=20para=20opciones=20deshabilitadas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- anemui-core/src/ui/CsMenuItem.tsx | 36 ++++++++++++++++--------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/anemui-core/src/ui/CsMenuItem.tsx b/anemui-core/src/ui/CsMenuItem.tsx index 4abc1a3..43dfa34 100644 --- a/anemui-core/src/ui/CsMenuItem.tsx +++ b/anemui-core/src/ui/CsMenuItem.tsx @@ -32,28 +32,30 @@ export class CsMenuItem extends BaseUiElement { this.values = _values; let count = 0; for (let i = 0; i < this.values.length; i++) { - if (!this.values[i].startsWith("~")) { + if (!this.values[i].startsWith("-")) { count++; } } if (this.container != undefined) { //alert("needs Update") let ul = this.container.getElementsByTagName("ul")[0] - ul.innerHTML = ""; - this.values.map((val, index) => { - var popOverAttrs = { - id: val.startsWith("~") ? val.substring(1) : val, - 'data-toggle': 'popover' - }; - if (!val.startsWith("-") && !val.startsWith("~")) { - /* if (val.startsWith("~")) { - addChild(ul, (
  • {val.substring(1)}
  • )) - } else { */ - addChild(ul, (
  • { this.select(index) }} href="#"> {val}
  • )) - // } - } - }); - this.drop.update() + if (ul) { + ul.innerHTML = ""; + this.values.map((val, index) => { + var popOverAttrs = { + id: val.startsWith("~") ? val.substring(1) : val, + 'data-toggle': 'popover' + }; + if (!val.startsWith("-") && !val.startsWith("~")) { + /* if (val.startsWith("~")) { + addChild(ul, (
  • {val.substring(1)}
  • )) + } else { */ + addChild(ul, (
  • { this.select(index) }} href="#"> {val}
  • )) + // } + } + }); + this.drop.update() + } } } @@ -86,7 +88,7 @@ export class CsMenuItem extends BaseUiElement { public render(_subTitle?: string, hasPopData: boolean = false): JSX.Element { let count = 0; for (let i = 0; i < this.values.length; i++) { - if (!this.values[i].startsWith("~") && !this.values[i].startsWith("-")) { + if (!this.values[i].startsWith("-")) { count++; } } From 6440b72c4101d0ddffdefa14a2ef13bf545f9bfd Mon Sep 17 00:00:00 2001 From: ManuelPruebas Date: Thu, 11 Dec 2025 09:08:59 +0100 Subject: [PATCH 10/28] =?UTF-8?q?Correcci=C3=B3n=20capas=20de=20incertidum?= =?UTF-8?q?bre?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- anemui-core/src/BaseApp.ts | 42 ++++++++++++++++++------- anemui-core/src/CsMap.tsx | 4 +-- anemui-core/src/LayerManager.ts | 12 +++++-- anemui-core/src/OpenLayersMap.ts | 36 ++++++++++----------- anemui-core/src/data/ChunkDownloader.ts | 18 ++++++++--- anemui-core/src/ui/LayerFrame.tsx | 19 +++++++++-- 6 files changed, 88 insertions(+), 43 deletions(-) diff --git a/anemui-core/src/BaseApp.ts b/anemui-core/src/BaseApp.ts index 369ce62..f01b98d 100644 --- a/anemui-core/src/BaseApp.ts +++ b/anemui-core/src/BaseApp.ts @@ -1,10 +1,10 @@ import { addChild, mount } from "tsx-create-element"; import { MainFrame } from "./ui/MainFrame"; -import { MenuBar, MenuBarListener } from './ui/MenuBar'; +import { MenuBar, MenuBarListener } from './ui/MenuBar'; import { CsMap } from "./CsMap"; import { DownloadFrame, DownloadIframe, DownloadOptionsDiv } from "./ui/DownloadFrame"; import LayerFrame from './ui/LayerFrame' -import PaletteFrame from "./ui/PaletteFrame"; +import PaletteFrame from "./ui/PaletteFrame"; import { CsLatLong, CsMapEvent, CsMapListener } from "./CsMapTypes"; import { DateSelectorFrame, DateFrameListener } from "./ui/DateFrame"; import { loadLatLongData } from "./data/CsDataLoader"; @@ -22,8 +22,8 @@ import { fromLonLat } from "ol/proj"; import Dygraph from "dygraphs"; import { Style } from 'ol/style.js'; import { FeatureLike } from "ol/Feature"; -import LeftBar from "./ui/LeftBar"; -import RightBar from "./ui/RightBar"; +import LeftBar from "./ui/LeftBar"; +import RightBar from "./ui/RightBar"; import Language from "./language/language"; import { renderers, folders, defaultRenderer } from "./tiles/Support"; import CsCookies from "./cookies/CsCookies"; @@ -594,8 +594,8 @@ export abstract class BaseApp implements CsMapListener, MenuBarListener, DateFra varName = varId } - let uncertainty = _timesJs.times[varId + UNCERTAINTY_LAYER] != undefined - + let uncertainty = _timesJs.times[varId + UNCERTAINTY_LAYER] != undefined + if (this.state == undefined) this.state = INITIAL_STATE; this.state = { ...this.state, @@ -681,19 +681,19 @@ export abstract class BaseApp implements CsMapListener, MenuBarListener, DateFra try { this.menuBar.update(); this.leftBar.update(); - this.csMap.updateDate(this.state.selectedTimeIndex, this.state); + await this.csMap.updateDate(this.state.selectedTimeIndex, this.state); this.csMap.updateRender(this.state.support); - + // Wait for data only if we have computed data tiles layer - if (computedDataTilesLayer && this.state.computedLayer) { + if (computedDataTilesLayer && this.state.computedLayer) { await this.waitForDataLoad(); } - + if (!dateChanged) this.dateSelectorFrame.update(); this.paletteFrame.update(); this.layerFrame.update(); this.changeUrl(); - + } catch (error) { console.error('Error during update:', error); // Continue with update even if there's an error @@ -947,12 +947,14 @@ export abstract class BaseApp implements CsMapListener, MenuBarListener, DateFra renderers[i]=renderers[i].substring(1) } } ) + console.log('enable renderer - renderers: ' + renderers) } public disableRenderer(i:number){ if(! renderers[i].startsWith("~")){ renderers[i]="~"+renderers[i]; } + console.log('disable renderer - renderers: ' + renderers) } public removeRenderer(i:number){ @@ -978,6 +980,22 @@ export abstract class BaseApp implements CsMapListener, MenuBarListener, DateFra } public showPercentileClockForPoint(latlng: CsLatLong, currentValue: number, historicalData: number[]): void { - + + } + + /** + * Resetea la capa de incertidumbre: la oculta y desactiva el checkbox + * Útil cuando se cambia de contexto (ej: cambiar horizonte de predicción) + * Usa el método toggleUncertaintyLayer del LayerFrame para mantener consistencia + */ + public resetUncertaintyLayer(): void { + // Llamar al método del LayerFrame que ya coordina todo el comportamiento + this.layerFrame.toggleUncertaintyLayer(false); + + // Asegurar que el checkbox en el DOM esté desactivado + const checkbox = document.getElementById('flexSwitchCheckChecked') as HTMLInputElement; + if (checkbox) { + checkbox.checked = false; + } } } diff --git a/anemui-core/src/CsMap.tsx b/anemui-core/src/CsMap.tsx index 121a3e2..994703d 100644 --- a/anemui-core/src/CsMap.tsx +++ b/anemui-core/src/CsMap.tsx @@ -124,8 +124,8 @@ public onMapClick(event: CsMapEvent): void { this.controller.putMarker(latLong); } - public updateDate(selectedTimeIndex: number, state: CsViewerData) { - this.controller.setDate(selectedTimeIndex,state); + public async updateDate(selectedTimeIndex: number, state: CsViewerData): Promise { + await this.controller.setDate(selectedTimeIndex,state); } public getParent():BaseApp{ diff --git a/anemui-core/src/LayerManager.ts b/anemui-core/src/LayerManager.ts index 5d1c433..42086d6 100644 --- a/anemui-core/src/LayerManager.ts +++ b/anemui-core/src/LayerManager.ts @@ -253,11 +253,19 @@ export class LayerManager { } public getUncertaintyLayer():(Image | WebGLTile)[] { - this.uncertaintyLayer = []; return this.uncertaintyLayer; } + public setUncertaintyLayer(layers: (Image | WebGLTile)[]) { + this.uncertaintyLayer = layers; + } + public showUncertaintyLayer(show: boolean) { - this.uncertaintyLayer[0].setVisible(show); + if (this.uncertaintyLayer && this.uncertaintyLayer.length > 0) { + this.uncertaintyLayer.forEach((layer) => { + layer.setVisible(show); + layer.changed(); // Forzar re-renderizado + }); + } } } \ No newline at end of file diff --git a/anemui-core/src/OpenLayersMap.ts b/anemui-core/src/OpenLayersMap.ts index b0695f7..5c5d251 100644 --- a/anemui-core/src/OpenLayersMap.ts +++ b/anemui-core/src/OpenLayersMap.ts @@ -222,7 +222,6 @@ protected fitInitialView(timesJs: CsTimesJsData, varId: string): void { this.buildFeatureLayers(); if (!isWmsEnabled) { this.buildDataTilesLayers(state, timesJs); - if (state.uncertaintyLayer) this.buildUncertaintyLayer(state, timesJs); } } @@ -465,7 +464,7 @@ private shouldShowPercentileClock(state: CsViewerData): boolean { } -public buildDataTilesLayers(state: CsViewerData, timesJs: CsTimesJsData): void { +public async buildDataTilesLayers(state: CsViewerData, timesJs: CsTimesJsData): Promise { let app = window.CsViewerApp; @@ -477,10 +476,8 @@ public buildDataTilesLayers(state: CsViewerData, timesJs: CsTimesJsData): void { // Si la capa de incertidumbre está activa, reconstruirla con el nuevo varId if (state.uncertaintyLayer) { this.safelyRemoveUncertaintyLayers(); - this.buildUncertaintyLayer(state, timesJs); - // Mostrar las capas después de construirlas - const lmgr = LayerManager.getInstance(); - lmgr.showUncertaintyLayer(true); + // Esperar a que se construyan las capas de incertidumbre + await this.buildUncertaintyLayer(state, timesJs); } if (!timesJs.portions[state.varId]) { @@ -576,14 +573,15 @@ public buildDataTilesLayers(state: CsViewerData, timesJs: CsTimesJsData): void { } // Fix for uncertainty layer with proper initialization - public buildUncertaintyLayer(state: CsViewerData, timesJs: CsTimesJsData): void { + public async buildUncertaintyLayer(state: CsViewerData, timesJs: CsTimesJsData): Promise { let lmgr = LayerManager.getInstance(); let app = window.CsViewerApp; // Safely remove existing uncertainty layers this.safelyRemoveUncertaintyLayers(); - this.uncertaintyLayer = lmgr.getUncertaintyLayer() || []; + // Siempre empezar con un array limpio + this.uncertaintyLayer = []; const uncertaintyVarId = state.varId + '_uncertainty'; if (!timesJs.portions[uncertaintyVarId]) { @@ -595,7 +593,7 @@ public buildDataTilesLayers(state: CsViewerData, timesJs: CsTimesJsData): void { let imageLayer: ImageLayer = new ImageLayer({ visible: false, opacity: 1.0, - zIndex: 100, + zIndex: 2000, // Entre capas de datos (100) y capa política (5000) source: null, className: 'uncertainty-layer' // Agregar clase CSS para la capa de incertidumbre }); @@ -622,7 +620,11 @@ public buildDataTilesLayers(state: CsViewerData, timesJs: CsTimesJsData): void { }); if (this.uncertaintyLayer.length > 0 && promises.length > 0) { - buildImages(promises, this.uncertaintyLayer, state, timesJs, app, this.ncExtents, true); + // Esperar a que se construyan las imágenes antes de registrar las capas + await buildImages(promises, this.uncertaintyLayer, state, timesJs, app, this.ncExtents, true); + + // Registrar las capas en LayerManager después de construirlas + lmgr.setUncertaintyLayer(this.uncertaintyLayer); lmgr.showUncertaintyLayer(false); } } @@ -646,6 +648,8 @@ public buildDataTilesLayers(state: CsViewerData, timesJs: CsTimesJsData): void { }); } this.uncertaintyLayer = []; + // Limpiar también el array en LayerManager + LayerManager.getInstance().setUncertaintyLayer([]); } public buildFeatureLayers () { @@ -668,8 +672,7 @@ public buildDataTilesLayers(state: CsViewerData, timesJs: CsTimesJsData): void { } ) } - // Fix for setDate method - public setDate(dateIndex: number, state: CsViewerData): void { + public async setDate(dateIndex: number, state: CsViewerData): Promise { try { if (isWmsEnabled) { if (this.dataWMSLayer) { @@ -681,14 +684,7 @@ public buildDataTilesLayers(state: CsViewerData, timesJs: CsTimesJsData): void { this.dataWMSLayer.refresh(); } } else { - this.buildDataTilesLayers(state, this.parent.getParent().getTimesJs()); - - // Safely remove uncertainty layers - this.safelyRemoveUncertaintyLayers(); - - if (state.uncertaintyLayer) { - this.buildUncertaintyLayer(state, this.parent.getParent().getTimesJs()); - } + await this.buildDataTilesLayers(state, this.parent.getParent().getTimesJs()); } } catch (error) { console.error('Error in setDate:', error); diff --git a/anemui-core/src/data/ChunkDownloader.ts b/anemui-core/src/data/ChunkDownloader.ts index 60455a0..7a08898 100644 --- a/anemui-core/src/data/ChunkDownloader.ts +++ b/anemui-core/src/data/ChunkDownloader.ts @@ -375,18 +375,22 @@ export async function buildImages(promises: Promise[], dataTilesLayer: // Obtener el nivel de zoom actual del mapa const currentZoom = app.getMap()?.getZoom() || 6; + // Para capas de incertidumbre, usar el varId con sufijo '_uncertainty' + const uncertaintyVarId = uncertaintyLayer ? status.varId + '_uncertainty' : status.varId; + for (let i = 0; i < filteredArrays.length; i++) { const filteredArray = filteredArrays[i]; - const width = timesJs.lonNum[status.varId + timesJs.portions[status.varId][i]]; - const height = timesJs.latNum[status.varId + timesJs.portions[status.varId][i]]; + const width = timesJs.lonNum[uncertaintyVarId + timesJs.portions[uncertaintyVarId][i]]; + const height = timesJs.latNum[uncertaintyVarId + timesJs.portions[uncertaintyVarId][i]]; let canvas: HTMLCanvasElement | null = null; try { canvas = await painterInstance.paintValues(filteredArray, width, height, minArray, maxArray, pxTransparent, uncertaintyLayer, currentZoom); if (canvas) { - const extent = ncExtents[timesJs.portions[status.varId][i]]; + const portionName = timesJs.portions[uncertaintyVarId][i]; + const extent = ncExtents[portionName]; const imageSource = new Static({ url: canvas.toDataURL('image/png'), @@ -398,8 +402,12 @@ export async function buildImages(promises: Promise[], dataTilesLayer: dataTilesLayer[i].setSource(imageSource); - // zIndex menor que 5000 para que los labels queden por encima - dataTilesLayer[i].setZIndex(4000 + i); + // zIndex diferenciado: incertidumbre (2000) por encima de datos (100) pero debajo de política (5000) + if (uncertaintyLayer) { + dataTilesLayer[i].setZIndex(2000 + i); // Capas de incertidumbre + } else { + dataTilesLayer[i].setZIndex(100 + i); // Capas de datos + } dataTilesLayer[i].setVisible(uncertaintyLayer ? false : true); dataTilesLayer[i].setOpacity(1.0); diff --git a/anemui-core/src/ui/LayerFrame.tsx b/anemui-core/src/ui/LayerFrame.tsx index b863e64..5ef3581 100644 --- a/anemui-core/src/ui/LayerFrame.tsx +++ b/anemui-core/src/ui/LayerFrame.tsx @@ -156,10 +156,25 @@ export default class LayerFrame extends BaseFrame { public toggleUncertaintyLayer (checked: boolean) { let ptMgr=PaletteManager.getInstance(); ptMgr.setUncertaintyLayerChecked(checked) + + // Verificar si el elemento existe antes de modificarlo let uncertaintyText = document.querySelector("#uncertainty-text") - uncertaintyText.innerHTML = this.parent.getTranslation('uncertainty') + ': ' + ptMgr.getUncertaintyLayerChecked() + if (uncertaintyText) { + uncertaintyText.innerHTML = this.parent.getTranslation('uncertainty') + ': ' + ptMgr.getUncertaintyLayerChecked() + } + let mgr=LayerManager.getInstance(); - mgr.showUncertaintyLayer(checked) + // Solo intentar mostrar/ocultar la capa si existe + const uncertaintyLayer = mgr.getUncertaintyLayer(); + if (uncertaintyLayer && uncertaintyLayer.length > 0) { + mgr.showUncertaintyLayer(checked) + + // Forzar renderizado completo del mapa + const csMap = this.parent.getMap(); + if (csMap && (csMap as any).controller && (csMap as any).controller.map) { + (csMap as any).controller.map.render(); + } + } } public renderUncertaintyFrame():JSX.Element { From cdb1d3966c423e76c42cf69ea7cc69bc853095ed Mon Sep 17 00:00:00 2001 From: ManuelPruebas Date: Tue, 16 Dec 2025 11:00:11 +0100 Subject: [PATCH 11/28] =?UTF-8?q?Nueva=20disposici=C3=B3n=20de=20bot=C3=B3?= =?UTF-8?q?n=20de=20incertidumbre=20en=20men=C3=BA=20superior?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- anemui-core/css/topbar.scss | 49 ++++++++ anemui-core/src/BaseApp.ts | 14 +-- anemui-core/src/PaletteManager.ts | 160 +------------------------ anemui-core/src/index.ts | 1 + anemui-core/src/ui/CsMenuItem.tsx | 74 ++++++++++++ anemui-core/src/ui/LayerFrame.tsx | 89 +------------- anemui-core/src/ui/MenuBar.tsx | 187 +++++++++++++++++++++++++----- 7 files changed, 290 insertions(+), 284 deletions(-) diff --git a/anemui-core/css/topbar.scss b/anemui-core/css/topbar.scss index 94e2b0d..649ebbb 100644 --- a/anemui-core/css/topbar.scss +++ b/anemui-core/css/topbar.scss @@ -520,6 +520,55 @@ input.selection-param-input { min-width: fit-content; } +// Contenedor del checkbox de incertidumbre +.inputDiv[role^="uncertainty-"] { + background: none !important; + box-shadow: none !important; + padding: 0 !important; + + &:hover { + background: none !important; + color: $color_10 !important; + } +} + +.menu-checkbox { + padding: 0; + display: flex; + align-items: center; + gap: 8px; + background: none !important; // Eliminar el fondo destacado + + .title { + white-space: nowrap; + font-size: 0.7em; + font-style: italic; + line-height: 1.2; + margin: 0; + padding: 0; + } + + .form-check { + margin: 0; + padding: 0; + display: flex; + align-items: center; + background: none !important; + + .form-check-input { + margin: 0; + cursor: pointer; + width: 2.5em; + height: 1.25em; + + &:focus { + box-shadow: none; + border-color: #0d6efd; + } + } + } +} + .menu-input { width: 150px !important; min-width: 150px !important; diff --git a/anemui-core/src/BaseApp.ts b/anemui-core/src/BaseApp.ts index f01b98d..e96648d 100644 --- a/anemui-core/src/BaseApp.ts +++ b/anemui-core/src/BaseApp.ts @@ -947,14 +947,12 @@ export abstract class BaseApp implements CsMapListener, MenuBarListener, DateFra renderers[i]=renderers[i].substring(1) } } ) - console.log('enable renderer - renderers: ' + renderers) } public disableRenderer(i:number){ if(! renderers[i].startsWith("~")){ renderers[i]="~"+renderers[i]; } - console.log('disable renderer - renderers: ' + renderers) } public removeRenderer(i:number){ @@ -986,16 +984,10 @@ export abstract class BaseApp implements CsMapListener, MenuBarListener, DateFra /** * Resetea la capa de incertidumbre: la oculta y desactiva el checkbox * Útil cuando se cambia de contexto (ej: cambiar horizonte de predicción) - * Usa el método toggleUncertaintyLayer del LayerFrame para mantener consistencia + * Usa el método toggleUncertaintyLayer del MenuBar para mantener consistencia */ public resetUncertaintyLayer(): void { - // Llamar al método del LayerFrame que ya coordina todo el comportamiento - this.layerFrame.toggleUncertaintyLayer(false); - - // Asegurar que el checkbox en el DOM esté desactivado - const checkbox = document.getElementById('flexSwitchCheckChecked') as HTMLInputElement; - if (checkbox) { - checkbox.checked = false; - } + // Llamar al método del MenuBar que coordina todo el comportamiento + this.getMenuBar().toggleUncertaintyLayer(false); } } diff --git a/anemui-core/src/PaletteManager.ts b/anemui-core/src/PaletteManager.ts index 6a8a5ba..e7be8d8 100644 --- a/anemui-core/src/PaletteManager.ts +++ b/anemui-core/src/PaletteManager.ts @@ -635,7 +635,6 @@ export class DotPatternPainter implements Painter { this.dotColor = color; this.dotOpacity = opacity; this.dotSpacing = spacing; - console.log('🟢 DotPatternPainter creado con:', { radius, color, opacity, spacing }); } public async paintValues( @@ -648,8 +647,7 @@ export class DotPatternPainter implements Painter { uncertaintyLayer: boolean, zoom?: number ): Promise { - console.log('🔵 DotPatternPainter.paintValues ejecutándose:', { width, height, uncertaintyLayer, dataLength: floatArray.length }); - + // Validar dimensiones width = Math.max(1, Math.floor(width)); height = Math.max(1, Math.floor(height)); @@ -705,10 +703,6 @@ export class DotPatternPainter implements Painter { } } - console.log('📊 Análisis de valores (primeros 100):', { - validValues, zeroValues, nanValues, minVal, maxVal, sampleValues - }); - // Pintar puntos cuadrados con espaciado solo donde hay incertidumbre let dotsDrawn = 0; let skippedDots = 0; @@ -743,20 +737,8 @@ export class DotPatternPainter implements Painter { } } - console.log('🎯 Puntos dibujados vs saltados:', { dotsDrawn, skippedDots }); - // Escribir los datos de píxeles al canvas context.putImageData(imageData, 0, 0); - - console.log('✅ DotPatternPainter terminado:', { dotsDrawn, totalPixels: width * height, alpha }); - - // Debug: Verificar que el canvas tiene contenido - let nonTransparentPixels = 0; - for (let i = 3; i < data.length; i += 4) { - if (data[i] > 0) nonTransparentPixels++; - } - console.log('🎨 Píxeles no transparentes en canvas:', nonTransparentPixels); - return canvas; } @@ -771,143 +753,3 @@ export class DotPatternPainter implements Painter { } } -/** - * HatchPatternPainter - Painter especializado para capa de incertidumbre - * Aplica un patrón de líneas diagonales que se adapta al nivel de zoom - */ -export class HatchPatternPainter implements Painter { - private hatchSpacing: number = 4; // Espaciado base entre líneas - private hatchAngle: number = 45; // Ángulo del tramado en grados - private hatchColor: string = '#000'; // Color del tramado - private hatchOpacity: number = 0.2; // Opacidad del tramado - - constructor(spacing: number = 4, angle: number = 45, color: string = '#000', opacity: number = 0.5) { - this.hatchSpacing = spacing; - this.hatchAngle = angle; - this.hatchColor = color; - this.hatchOpacity = opacity; - } - - public async paintValues( - floatArray: number[], - width: number, - height: number, - minArray: number, - maxArray: number, - pxTransparent: number, - uncertaintyLayer: boolean, - zoom?: number - ): Promise { - // Validar dimensiones - width = Math.max(1, Math.floor(width)); - height = Math.max(1, Math.floor(height)); - - if (!isFinite(width) || !isFinite(height)) { - console.error('Invalid canvas dimensions:', width, height); - width = 1; - height = 1; - } - - let canvas: HTMLCanvasElement = document.createElement('canvas'); - let context: CanvasRenderingContext2D = canvas.getContext('2d'); - canvas.width = width; - canvas.height = height; - - // Crear una máscara con los datos válidos - let maskCanvas: HTMLCanvasElement = document.createElement('canvas'); - let maskContext: CanvasRenderingContext2D = maskCanvas.getContext('2d'); - maskCanvas.width = width; - maskCanvas.height = height; - - let maskData: ImageData = maskContext.getImageData(0, 0, width, height); - let maskBitmap: Uint32Array = new Uint32Array(maskData.data.buffer); - - // Crear máscara: blanco donde hay datos válidos, transparente donde no - for (let y: number = 0; y < height; y++) { - for (let x: number = 0; x < width; x++) { - let ncIndex: number = x + y * width; - let value: number = floatArray[ncIndex]; - let pxIndex: number = x + ((height - 1) - y) * width; - - // Si el valor es válido (no NaN y finito), marcarlo en la máscara - if (!isNaN(value) && isFinite(value)) { - maskBitmap[pxIndex] = 0xFFFFFFFF; // Blanco opaco - } else { - maskBitmap[pxIndex] = 0x00000000; // Transparente - } - } - } - - maskContext.putImageData(maskData, 0, 0); - - // Ajustar el espaciado según el nivel de zoom del mapa - // A mayor zoom, menor espaciado (líneas más juntas) - // Zoom típico: 5-11, donde 6 es el inicial - const zoomLevel = zoom || 6; - const zoomFactor = Math.pow(2, zoomLevel - 6); // Factor exponencial basado en nivel de zoom - let adaptiveSpacing = Math.max(1, Math.floor(this.hatchSpacing / zoomFactor)); - - // Dibujar el patrón de tramado - context.strokeStyle = this.hatchColor; - context.globalAlpha = this.hatchOpacity; - context.lineWidth = 1; - - // Guardar el contexto para restaurar después - context.save(); - - // Aplicar la máscara usando globalCompositeOperation - // Primero dibujamos el patrón, luego aplicamos la máscara - - // Dibujar líneas diagonales - const angleRad = (this.hatchAngle * Math.PI) / 180; - const diagonal = Math.sqrt(width * width + height * height); - - context.beginPath(); - - // Calcular número de líneas necesarias - const numLines = Math.ceil(diagonal / adaptiveSpacing); - - for (let i = -numLines; i <= numLines; i++) { - const offset = i * adaptiveSpacing; - - // Calcular puntos de inicio y fin de la línea diagonal - const x1 = -diagonal; - const y1 = offset; - const x2 = diagonal; - const y2 = offset; - - // Rotar y trasladar - const cos = Math.cos(angleRad); - const sin = Math.sin(angleRad); - - const rx1 = x1 * cos - y1 * sin + width / 2; - const ry1 = x1 * sin + y1 * cos + height / 2; - const rx2 = x2 * cos - y2 * sin + width / 2; - const ry2 = x2 * sin + y2 * cos + height / 2; - - context.moveTo(rx1, ry1); - context.lineTo(rx2, ry2); - } - - context.stroke(); - - // Aplicar la máscara: solo mantener el tramado donde hay datos válidos - context.globalCompositeOperation = 'destination-in'; - context.globalAlpha = 1.0; - context.drawImage(maskCanvas, 0, 0); - - context.restore(); - - return canvas; - } - - public getColorString(val: number, min: number, max: number): string { - // Para compatibilidad con la interfaz Painter - return this.hatchColor; - } - - public getValIndex(val: number): number { - // Para compatibilidad con la interfaz Painter - return 0; - } -} \ No newline at end of file diff --git a/anemui-core/src/index.ts b/anemui-core/src/index.ts index d94e4b3..46eabf8 100644 --- a/anemui-core/src/index.ts +++ b/anemui-core/src/index.ts @@ -4,6 +4,7 @@ export * from './PaletteManager'; export * from './ServiceApp'; export { BaseApp } from './BaseApp'; export { InfoDiv } from './ui/InfoPanel'; +export { default as Language } from './language/language'; // Export Support.ts items for backward compatibility export { renderers, getFolders, defaultRenderer } from './tiles/Support'; export const defaultElement = "Rejilla"; diff --git a/anemui-core/src/ui/CsMenuItem.tsx b/anemui-core/src/ui/CsMenuItem.tsx index 43dfa34..66b345e 100644 --- a/anemui-core/src/ui/CsMenuItem.tsx +++ b/anemui-core/src/ui/CsMenuItem.tsx @@ -184,6 +184,80 @@ export class CsMenuItem extends BaseUiElement { } +export interface CsMenuCheckboxListener { + checkboxChanged(origin: CsMenuCheckbox, checked: boolean): void; +} + +export class CsMenuCheckbox extends BaseUiElement { + public id: string; + private title: string; + private checked: boolean; + private listener: CsMenuCheckboxListener; + + constructor(_id: string, _title: string, _listener: CsMenuCheckboxListener, _checked: boolean = false) { + super(); + this.id = _id; + this.title = _title; + this.checked = _checked; + this.listener = _listener; + } + + public setTitle(_title: string) { + this.title = _title; + if (this.container != undefined) { + const titleSpan = this.container.querySelector(".title"); + if (titleSpan) { + titleSpan.innerHTML = this.title; + } + } + } + + public setChecked(_checked: boolean) { + this.checked = _checked; + if (this.container) { + const inputElement = this.container.querySelector(`#${this.id}`) as HTMLInputElement; + if (inputElement) { + inputElement.checked = _checked; + } + } + } + + public getChecked(): boolean { + return this.checked; + } + + public render(): JSX.Element { + return ( +
    + {this.title} +
    + ) => { + this.checked = e.target.checked; + this.listener.checkboxChanged(this, this.checked); + }} + /> +
    +
    + ); + } + + public build(_container?: HTMLDivElement) { + this.container = _container; + } + + public config(hidden: boolean) { + if (this.container) { + this.container.hidden = hidden; + } + } +} + export class CsMenuInput extends BaseUiElement { public id: string; private title: string diff --git a/anemui-core/src/ui/LayerFrame.tsx b/anemui-core/src/ui/LayerFrame.tsx index 5ef3581..5b4ef33 100644 --- a/anemui-core/src/ui/LayerFrame.tsx +++ b/anemui-core/src/ui/LayerFrame.tsx @@ -11,8 +11,7 @@ export default class LayerFrame extends BaseFrame { protected slider: Slider private baseDiv: HTMLElement private polDiv: HTMLElement - private trpDiv: HTMLElement - private uncertaintyFrame: HTMLElement; + private trpDiv: HTMLElement public render():JSX.Element{ let self=this; @@ -20,11 +19,8 @@ export default class LayerFrame extends BaseFrame { let lmgr = LayerManager.getInstance(); let baseLayers=lmgr.getBaseLayerNames(); let topLayers=lmgr.getTopLayerNames(); - let uncertaintyLayer = this.parent.getState().uncertaintyLayer; let selected = initialZoom >= 6.00? ["EUMETSAT","PNOA"]:["ARCGIS"]; // --- Provisional, ver la manera de configurar let i: number = 0; - // mgr.setUncertaintyLayerChecked(true) // ------------ ORIGINAL, por defecto está activada - // mgr.setUncertaintyLayerChecked(false) let element= (
    @@ -103,26 +99,6 @@ export default class LayerFrame extends BaseFrame {
    -
    - {uncertaintyLayer && -
    -
    this.toggleSelect('uncDiv')}> - - - {this.parent.getTranslation('uncertainty')}: {mgr.getUncertaintyLayerChecked()} - -
    -
    -
    this.toggleSelect('uncDiv')}> - -
    -
    - self.toggleUncertaintyLayer(event.target.checked)} /> -
    -
    -
    - } -
    ); return element; @@ -153,53 +129,6 @@ export default class LayerFrame extends BaseFrame { // this.container.querySelector("div.layerFrame").classList.remove("visible") } - public toggleUncertaintyLayer (checked: boolean) { - let ptMgr=PaletteManager.getInstance(); - ptMgr.setUncertaintyLayerChecked(checked) - - // Verificar si el elemento existe antes de modificarlo - let uncertaintyText = document.querySelector("#uncertainty-text") - if (uncertaintyText) { - uncertaintyText.innerHTML = this.parent.getTranslation('uncertainty') + ': ' + ptMgr.getUncertaintyLayerChecked() - } - - let mgr=LayerManager.getInstance(); - // Solo intentar mostrar/ocultar la capa si existe - const uncertaintyLayer = mgr.getUncertaintyLayer(); - if (uncertaintyLayer && uncertaintyLayer.length > 0) { - mgr.showUncertaintyLayer(checked) - - // Forzar renderizado completo del mapa - const csMap = this.parent.getMap(); - if (csMap && (csMap as any).controller && (csMap as any).controller.map) { - (csMap as any).controller.map.render(); - } - } - } - - public renderUncertaintyFrame():JSX.Element { - let mgr=PaletteManager.getInstance(); - mgr.setUncertaintyLayerChecked(false) - return ( -
    -
    this.toggleSelect('uncDiv')}> - - - {this.parent.getTranslation('uncertainty')}: {mgr.getUncertaintyLayerChecked()} - -
    -
    -
    this.toggleSelect('uncDiv')}> - -
    -
    - this.toggleUncertaintyLayer(event.target.checked)} /> -
    -
    -
    - ); - } - public build(){ this.container = document.getElementById("layer-frame") as HTMLDivElement this.baseDiv = document.getElementById('base-div') as HTMLElement; @@ -248,21 +177,5 @@ export default class LayerFrame extends BaseFrame { this.container.querySelector(".layerFrame span[aria-label=base]").textContent= this.parent.getTranslation('base_layer') +": "+lmgr.getBaseSelected(); this.container.querySelector(".layerFrame span[aria-label=transparency]").textContent= this.parent.getTranslation('transparency') +": "+mgr.getTransparency(); this.container.querySelector(".layerFrame span[aria-label=top]").textContent= this.parent.getTranslation('top_layer') +": "+lmgr.getTopSelected(); - let uncertaintyLayer = this.parent.getState().uncertaintyLayer; - - this.uncertaintyFrame = this.container.querySelector("#unc-div") - if (uncertaintyLayer) { - this.uncertaintyFrame.hidden = false; - if (this.uncertaintyFrame.children.length == 0) { - addChild(this.uncertaintyFrame,this.renderUncertaintyFrame()) - } - } else { - this.uncertaintyFrame.hidden = true; - if (this.uncertaintyFrame.children.length > 0) { - while (this.uncertaintyFrame.firstChild) { - this.uncertaintyFrame.removeChild(this.uncertaintyFrame.firstChild); - } - } - } } } \ No newline at end of file diff --git a/anemui-core/src/ui/MenuBar.tsx b/anemui-core/src/ui/MenuBar.tsx index 03d5242..f92b4c0 100644 --- a/anemui-core/src/ui/MenuBar.tsx +++ b/anemui-core/src/ui/MenuBar.tsx @@ -1,6 +1,6 @@ import { createElement, addChild } from 'tsx-create-element'; import "../../css/anemui-core.scss" -import { CsMenuItem, CsMenuInput, CsMenuItemListener } from './CsMenuItem'; +import { CsMenuItem, CsMenuInput, CsMenuCheckbox, CsMenuItemListener, CsMenuCheckboxListener } from './CsMenuItem'; import { BaseFrame, BaseUiElement, mouseOverFrame } from './BaseFrame'; import { BaseApp } from '../BaseApp'; import { logo, logoStyle, hasButtons, hasSpSupport, hasSubVars, hasTpSupport, hasClimatology, hasVars, hasSelection, hasSelectionParam, hasUnits, varHasPopData, sbVarHasPopData } from "../Env"; @@ -86,6 +86,13 @@ export class MenuBar extends BaseFrame { private displayUnits: HTMLDivElement; private units: CsMenuItem; + private displayUncertainty: HTMLDivElement; + private uncertaintyCheckbox: CsMenuCheckbox; + private uncertaintyAssociatedRole: string; // Role del botón al que está asociado + private uncertaintyCssClass: string; // Clase CSS del botón asociado + private uncertaintyRole: string; // Role dinámico del uncertainty basado en el botón asociado + private uncertaintyId: string; // ID dinámico del uncertainty basado en el botón asociado + constructor(_parent: BaseApp, _listener: MenuBarListener) { super(_parent) @@ -145,6 +152,9 @@ export class MenuBar extends BaseFrame { }, }); + // El uncertainty checkbox se creará dinámicamente en setUncertaintyAssociation() + // con un id y role basados en el botón asociado + this.extraMenuItems = [] this.extraMenuInputs = [] this.extraBtns = [] @@ -367,6 +377,7 @@ export class MenuBar extends BaseFrame { addChild(this.displayUnits, this.units.render(this.parent.getState().subVarName, false)); this.units.build(this.displayUnits); } + if (hasClimatology) { this.extraDisplays.forEach((dsp) => { addChild(this.inputsFrame, this.renderDisplay(dsp, 'climBtn')); @@ -516,6 +527,38 @@ export class MenuBar extends BaseFrame { this.displayVar.classList.add('display:none') } + /** + * Maneja el cambio de estado del checkbox de incertidumbre + * @param checked - Estado del checkbox (true = mostrar capa, false = ocultar capa) + */ + public toggleUncertaintyLayer(checked: boolean): void { + const PaletteManager = require('../PaletteManager').PaletteManager; + const LayerManager = require('../LayerManager').LayerManager; + + const ptMgr = PaletteManager.getInstance(); + const lmgr = LayerManager.getInstance(); + + // Actualizar el estado en PaletteManager + ptMgr.setUncertaintyLayerChecked(checked); + + // Actualizar el checkbox solo si existe + if (this.uncertaintyCheckbox) { + this.uncertaintyCheckbox.setChecked(checked); + } + + // Mostrar/ocultar la capa si existe + const uncertaintyLayer = lmgr.getUncertaintyLayer(); + if (uncertaintyLayer && uncertaintyLayer.length > 0) { + lmgr.showUncertaintyLayer(checked); + + // Forzar renderizado del mapa + const csMap = this.parent.getMap(); + if (csMap && (csMap as any).controller && (csMap as any).controller.map) { + (csMap as any).controller.map.render(); + } + } + } + public update(): void { if (!hasButtons) return @@ -548,6 +591,71 @@ export class MenuBar extends BaseFrame { } else { this.hideClimFrame() } + + // Gestionar checkbox de incertidumbre dinámicamente + const uncertaintyLayer = this.parent.getState().uncertaintyLayer; + + if (uncertaintyLayer && this.uncertaintyAssociatedRole && this.uncertaintyCssClass) { + const PaletteManager = require('../PaletteManager').PaletteManager; + const ptMgr = PaletteManager.getInstance(); + + // Si no existe el checkbox, crearlo + const isNewCheckbox = !this.displayUncertainty; + if (isNewCheckbox) { + // Activar la capa de incertidumbre por defecto ANTES de crear el checkbox + ptMgr.setUncertaintyLayerChecked(true); + + // Encontrar el botón asociado + const associatedButton = this.container.querySelector(`[role="${this.uncertaintyAssociatedRole}"]`) as HTMLElement; + if (associatedButton) { + // Crear el display del checkbox con la clase CSS del botón asociado + // Usar el role dinámico basado en el botón asociado + let dspUncertainty: simpleDiv = { role: this.uncertaintyRole, title: 'Incertidumbre', subTitle: '' }; + const uncertaintyElement = this.renderDisplay(dspUncertainty, this.uncertaintyCssClass); + + // Agregar el elemento al DOM + addChild(this.inputsFrame, uncertaintyElement); + + // Obtener el elemento recién agregado y reposicionarlo después del botón asociado + this.displayUncertainty = this.container.querySelector(`[role="${this.uncertaintyRole}"]`) as HTMLDivElement; + if (this.displayUncertainty && associatedButton.nextSibling) { + this.inputsFrame.insertBefore(this.displayUncertainty, associatedButton.nextSibling); + } + + // Configurar el checkbox - establecer checked=true ANTES de renderizar + this.uncertaintyCheckbox.setChecked(true); + addChild(this.displayUncertainty, this.uncertaintyCheckbox.render()); + this.uncertaintyCheckbox.build(this.displayUncertainty); + + // Activar la visualización de la capa de incertidumbre con un pequeño delay + // para dar tiempo a que la capa se cargue en el LayerManager + setTimeout(() => { + this.toggleUncertaintyLayer(true); + }, 200); + } + } + + // Mostrar y actualizar el estado del checkbox + if (this.displayUncertainty) { + this.displayUncertainty.hidden = false; + // Si no es nuevo, mantener el estado actual del PaletteManager + if (!isNewCheckbox) { + const isChecked = ptMgr.getUncertaintyLayerChecked(); + this.uncertaintyCheckbox.setChecked(isChecked); + + // Si el checkbox está activo, reactivar la capa con delay + // para dar tiempo a que se cargue la nueva capa de uncertainty + if (isChecked) { + setTimeout(() => { + this.toggleUncertaintyLayer(true); + }, 200); + } + } + } + } else if (this.displayUncertainty) { + // Si no hay capa de incertidumbre, ocultar el checkbox + this.displayUncertainty.hidden = true; + } } public showMenusForClimatology(): void { if (!this.climBtnArray || this.climBtnArray.length === 0) { @@ -621,7 +729,7 @@ export class MenuBar extends BaseFrame { } } - public setExtraDisplay(type: number, id: string, displayTitle: string, options: string[]) { + public setExtraDisplay(type: number, id: string, displayTitle: string, options: string[], cssClass?: string, hasUncertainty?: boolean) { this.extraDisplays.push({ role: id, title: displayTitle, subTitle: options[0] }) let listener = this.listener @@ -645,6 +753,11 @@ export class MenuBar extends BaseFrame { break; } + // Si este botón debe tener un checkbox de incertidumbre asociado, configurarlo automáticamente + // La clase CSS es necesaria para saber qué tipo de botón es (predBtn, climBtn, etc.) + if (hasUncertainty && cssClass) { + this.setUncertaintyAssociation(id, cssClass); + } } public hideExtraMenuItem(role: string): void { @@ -807,39 +920,61 @@ export class MenuBar extends BaseFrame { } public setUnits(_units: string[]) { - this.units.setValues(_units); -} + this.units.setValues(_units); + } -public updateUnitsDisplay(value: string) { - if (this.displayUnits) { - const subtitle = this.displayUnits.querySelector('.sub-title'); - if (subtitle) { - subtitle.innerHTML = value; + public updateUnitsDisplay(value: string) { + if (this.displayUnits) { + const subtitle = this.displayUnits.querySelector('.sub-title'); + if (subtitle) { + subtitle.innerHTML = value; + } } } -} -public hideUnits() { - if (this.displayUnits) { - this.displayUnits.hidden = true; + public hideUnits() { + if (this.displayUnits) { + this.displayUnits.hidden = true; + } } -} -public showUnits() { - if (this.displayUnits) { - this.displayUnits.hidden = false; + public showUnits() { + if (this.displayUnits) { + this.displayUnits.hidden = false; + } } -} -private getLegendTitleForUnit(unit: string): string { - switch(unit) { - case 'Km/h': return 'Km/h'; - case 'm/s': return 'm/s'; - case 'Nudos': return 'Nudos'; - case 'Escala WMO/Beaufort': return 'Beaufort'; - default: return unit; + /** + * Configura el checkbox de incertidumbre para que se cree asociado a un botón específico + * @param associatedRole - Role del botón al que se asociará (ej: 'horizontePred') + * @param cssClass - Clase CSS del botón (ej: 'predBtn', 'climBtn') + */ + public setUncertaintyAssociation(associatedRole: string, cssClass: string): void { + let self = this; + this.uncertaintyAssociatedRole = associatedRole; + this.uncertaintyCssClass = cssClass; + + // Generar role e id dinámicos basados en el botón asociado + this.uncertaintyRole = `uncertainty-${associatedRole}`; + this.uncertaintyId = `UncertaintyCheckbox-${associatedRole}`; + + // Crear el checkbox con el id dinámico + this.uncertaintyCheckbox = new CsMenuCheckbox(this.uncertaintyId, "Incertidumbre", { + checkboxChanged(origin, checked) { + self.toggleUncertaintyLayer(checked); + }, + }, false); // Inicialmente no marcado + } + + private getLegendTitleForUnit(unit: string): string { + switch(unit) { + case 'Km/h': return 'Km/h'; + case 'm/s': return 'm/s'; + case 'Nudos': return 'Nudos'; + case 'Escala WMO/Beaufort': return 'Beaufort'; + default: return unit; + } } -} // public selectFirstSpatialSupportValue(): void { // this.spatialSupport.selectFirstValidValue(); From 03be51de850b5d45f97997cde501aeb255f4a9a3 Mon Sep 17 00:00:00 2001 From: ManuelPruebas Date: Wed, 17 Dec 2025 10:25:55 +0100 Subject: [PATCH 12/28] =?UTF-8?q?Implementaci=C3=B3n=20de=20calendario=20b?= =?UTF-8?q?iling=C3=BCe=20espa=C3=B1ol/ingl=C3=A9s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- anemui-core/src/ui/DateFrame.tsx | 137 ++++++++++-- anemui-core/src/ui/Graph.tsx | 359 +------------------------------ 2 files changed, 126 insertions(+), 370 deletions(-) diff --git a/anemui-core/src/ui/DateFrame.tsx b/anemui-core/src/ui/DateFrame.tsx index 034af1b..a0be6db 100644 --- a/anemui-core/src/ui/DateFrame.tsx +++ b/anemui-core/src/ui/DateFrame.tsx @@ -4,10 +4,11 @@ import 'bootstrap-datepicker' // import * as $ from "jquery"; import $ from "jquery"; import 'bootstrap-slider' -import { default as Slider } from 'bootstrap-slider'; +import { default as Slider } from 'bootstrap-slider'; import { BaseFrame, BaseUiElement, mouseOverFrame } from './BaseFrame'; import { BaseApp } from '../BaseApp'; import { CsDropdown, CsDropdownListener } from './CsDropdown'; +import { locale } from '../Env'; export interface DateFrameListener { @@ -69,6 +70,100 @@ export class DateSelectorFrame extends BaseFrame { super(_parent) this.listener = _listener; let self = this + this.configureDatepickerLocale(); + } + + /** + * Configura el locale del datepicker según el idioma del sistema + * Añade traducciones personalizadas si bootstrap-datepicker no las tiene nativas + */ + private configureDatepickerLocale(): void { + // bootstrap-datepicker tiene soporte nativo para 'es' e 'en' + // pero vamos a asegurar que estén correctamente configurados + + // Cast a any para acceder a la propiedad dates que existe en runtime pero no en los tipos + const datepicker = $.fn.datepicker as any; + + if (locale === 'es' && datepicker && datepicker.dates) { + // Configuración española personalizada + datepicker.dates['es'] = { + days: ["Domingo", "Lunes", "Martes", "Miércoles", "Jueves", "Viernes", "Sábado"], + daysShort: ["Dom", "Lun", "Mar", "Mié", "Jue", "Vie", "Sáb"], + daysMin: ["Do", "Lu", "Ma", "Mi", "Ju", "Vi", "Sá"], + months: ["Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", + "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"], + monthsShort: ["Ene", "Feb", "Mar", "Abr", "May", "Jun", + "Jul", "Ago", "Sep", "Oct", "Nov", "Dic"], + today: "Hoy", + clear: "Borrar", + titleFormat: "MM yyyy", + weekStart: 1 + }; + } else if (locale === 'en' && datepicker && datepicker.dates) { + // Configuración inglesa (bootstrap-datepicker tiene 'en' por defecto, pero lo aseguramos) + datepicker.dates['en'] = { + days: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"], + daysShort: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"], + daysMin: ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"], + months: ["January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December"], + monthsShort: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], + today: "Today", + clear: "Clear", + titleFormat: "MM yyyy", + weekStart: 0 + }; + } + } + + /** + * Obtiene el código de idioma para bootstrap-datepicker según el locale configurado + */ + private getDatepickerLanguage(): string { + // bootstrap-datepicker acepta 'es' o 'en' + return locale === 'en' ? 'en' : 'es'; + } + + /** + * Obtiene el formato de fecha para visualización según el locale + */ + private getDateFormat(): string { + // Español: dd/mm/yyyy (formato europeo con barras) + // Inglés: yyyy-mm-dd (formato ISO) + return locale === 'en' ? 'yyyy-mm-dd' : 'dd/mm/yyyy'; + } + + /** + * Convierte una fecha del formato yyyy-mm-dd (interno) al formato de visualización + */ + private formatDateForDisplay(dateStr: string): string { + if (!dateStr || locale === 'en') { + return dateStr; // En inglés no hay conversión necesaria + } + + // Convertir yyyy-mm-dd a dd/mm/yyyy para español + const parts = dateStr.split('-'); + if (parts.length === 3) { + return `${parts[2]}/${parts[1]}/${parts[0]}`; + } + return dateStr; + } + + /** + * Convierte una fecha del formato de visualización al formato interno yyyy-mm-dd + */ + private parseDateFromDisplay(dateStr: string): string { + if (!dateStr || locale === 'en') { + return dateStr; // En inglés no hay conversión necesaria + } + + // Convertir dd/mm/yyyy a yyyy-mm-dd para español + const parts = dateStr.split('/'); + if (parts.length === 3) { + return `${parts[2]}-${parts[1]}-${parts[0]}`; + } + return dateStr; } public setValidDates(_dates: string[], _varChanged: boolean = false): void { @@ -188,18 +283,20 @@ export class DateSelectorFrame extends BaseFrame { switch(this.mode) { case DateFrameMode.DateFrameDate: - this.datepicker.datepicker('setDate', this.dates[safeIndex]); + // Convertir formato para visualización en español + const displayDate = this.formatDateForDisplay(this.dates[safeIndex]); + this.datepicker.datepicker('setDate', displayDate); if (this.dates.length > 1) { - this.datepicker.datepicker('setStartDate', this.dates[0]); - this.datepicker.datepicker('setEndDate', this.dates[this.dates.length - 1]); + this.datepicker.datepicker('setStartDate', this.formatDateForDisplay(this.dates[0])); + this.datepicker.datepicker('setEndDate', this.formatDateForDisplay(this.dates[this.dates.length - 1])); } break; case DateFrameMode.DateFrameMonth: if (this.months && this.months.length > 0) { - this.datepicker.datepicker('setDate', this.months[safeIndex]); + this.datepicker.datepicker('setDate', this.formatDateForDisplay(this.months[safeIndex])); if (this.months.length > 1) { - this.datepicker.datepicker('setStartDate', this.months[0]); - this.datepicker.datepicker('setEndDate', this.months[this.months.length - 1]); + this.datepicker.datepicker('setStartDate', this.formatDateForDisplay(this.months[0])); + this.datepicker.datepicker('setEndDate', this.formatDateForDisplay(this.months[this.months.length - 1])); } } break; @@ -379,17 +476,21 @@ export class DateSelectorFrame extends BaseFrame { } let options: DatepickerOptions; let pickerId: string + const datepickerLang = this.getDatepickerLanguage(); + const dateFormat = this.getDateFormat(); + const weekStart = locale === 'en' ? 0 : 1; // Inglés: domingo=0, Español: lunes=1 + switch(this.mode) { case DateFrameMode.DateFrameDate: options = { - format: "yyyy-mm-dd", + format: dateFormat, autoclose: true, beforeShowDay: (date: Date) => this.isDateValid(date), beforeShowMonth:(date: Date) => this.isMonthValid(date), beforeShowYear:(date: Date) => this.isYearValid(date), maxViewMode:'years', - language:'es-ES', - weekStart:1, + language: datepickerLang, + weekStart: weekStart, } pickerId = 'datePicker' break; @@ -400,7 +501,7 @@ export class DateSelectorFrame extends BaseFrame { startView: 'months', minViewMode: 'months', maxViewMode:'years', - language:'es-ES' + language: datepickerLang } pickerId = 'monthPicker' break; @@ -410,7 +511,7 @@ export class DateSelectorFrame extends BaseFrame { autoclose: true, minViewMode: "years", maxViewMode: "years", - language: "es" + language: datepickerLang } pickerId = 'seasonPicker' this.pickerNotClicked = true; @@ -421,7 +522,7 @@ export class DateSelectorFrame extends BaseFrame { autoclose: true, minViewMode: "years", maxViewMode: "years", - language: "es", + language: datepickerLang, beforeShowYear: (date: Date) => { const year = date.getFullYear().toString(); const isValid = this.yearIndex && this.yearIndex[year] !== undefined; @@ -562,21 +663,21 @@ export class DateSelectorFrame extends BaseFrame { }) // @ts-ignore this.slider._setText = function (element: any, text: any) {} - this.container.getElementsByClassName("tooltip-inner")[0].textContent=this.dates[endDate] + this.container.getElementsByClassName("tooltip-inner")[0].textContent=this.formatDateForDisplay(this.dates[endDate]) this.slider.on('slideStop',(val:number)=>{ if(val==this.parent.getState().selectedTimeIndex)return; this.parent.getState().selectedTimeIndex=val; - this.datepicker.datepicker('setDate', this.dates[val]) + this.datepicker.datepicker('setDate', this.formatDateForDisplay(this.dates[val])) if (this.mode == DateFrameMode.DateFrameSeason) { - let season = this.getSeason(this.dates[val]) + let season = this.getSeason(this.dates[val]) } this.parent.update(); }) this.slider.on('slide',(val)=>{ - this.container.getElementsByClassName("tooltip-inner")[0].textContent=this.dates[val] + this.container.getElementsByClassName("tooltip-inner")[0].textContent=this.formatDateForDisplay(this.dates[val]) }) this.slider.on('slideStop',(val)=>{ - this.container.getElementsByClassName("tooltip-inner")[0].textContent=this.dates[val] + this.container.getElementsByClassName("tooltip-inner")[0].textContent=this.formatDateForDisplay(this.dates[val]) }) } diff --git a/anemui-core/src/ui/Graph.tsx b/anemui-core/src/ui/Graph.tsx index 0b6ffa9..6448434 100644 --- a/anemui-core/src/ui/Graph.tsx +++ b/anemui-core/src/ui/Graph.tsx @@ -229,8 +229,8 @@ public showGraph(data: any, latlng: CsLatLong = { lat: 0.0, lng: 0.0 }, station: console.log('Data type:', typeof data); console.log('Has currentValue:', data?.currentValue !== undefined); console.log('Has historicalData:', data?.historicalData !== undefined); - - this.graphSubTitle = station.length != 0? ' - ' + station['name'] : ' ' + latlng.lat.toFixed(2) + ' N , ' + latlng.lng.toFixed(2) + ' E'; + + this.graphSubTitle = station.length != 0? ' - ' + station['name'] : ''; this.container.hidden = false; if (Object.keys(station).length != 0) this.enableStationDwButton(station); @@ -303,8 +303,8 @@ public showGraph(data: any, latlng: CsLatLong = { lat: 0.0, lng: 0.0 }, station: digitsAfterDecimal: 3, delimiter: ";", title: this.graphTitle + this.graphSubTitle, - ylabel: this.parent.getState().legendTitle, - xlabel: dateText, + ylabel: this.yLabel || this.parent.getState().legendTitle, + xlabel: this.xLabel || dateText, showRangeSelector: true, xValueParser: function (str: any): number { let readTime: string @@ -352,351 +352,6 @@ public showGraph(data: any, latlng: CsLatLong = { lat: 0.0, lng: 0.0 }, station: return graph; } - private showMonthlyViewBar(): void { - if (!this.currentGraph) return; - - const self = this; - - // Actualizar datos del año actual - this.updateGraphForYear(); - - // Restaurar configuración de barras con meses - this.currentGraph.updateOptions({ - showRangeSelector: false, - plotter: function (e: any) { - let ctx = e.drawingContext; - let area = e.plotArea; - - // Configurar estilo de las barras - ctx.fillStyle = "#4285F4"; - ctx.strokeStyle = "#1a73e8"; - ctx.lineWidth = 1; - - // Calcular ancho de cada barra - let barWidth = Math.max(1, (area.w / e.points.length) * 0.8); - - // Dibujar las barras - for (let i = 0; i < e.points.length; i++) { - let point = e.points[i]; - if (!isNaN(point.yval) && point.yval !== null) { - let barHeight = Math.abs(point.canvasy - area.y - area.h); - let barX = point.canvasx - barWidth / 2; - let barY = Math.min(point.canvasy, area.y + area.h); - - ctx.fillRect(barX, barY, barWidth, barHeight); - ctx.strokeRect(barX, barY, barWidth, barHeight); - } - } - }, - axes: { - x: { - valueFormatter: function (millis: any) { - let fecha = new Date(millis); - return self.formatDate(fecha); - }, - axisLabelFormatter: function(number: any) { - const fecha = new Date(number); - if (fecha.getDate() === 1 || fecha.getDate() === 1) { - return self.parent.getMonthName(fecha.getMonth(), true); - } - return ''; - }, - pixelsPerLabel: 50 - }, - y: { - valueFormatter: function (millis: any) { - return " " + (millis < 0.01? millis.toFixed(3) : millis.toFixed(2)); - } - } - } - }); - } - - private showMonthlyView(): void { - if (!this.currentGraph) return; - - // Restaurar vista mensual con puntos coloreados por superficie - this.updateGraphForYear(); - - const self = this; - - // Función para obtener color según superficie afectada (0-100) y tipo de ola - const getColorBySurface = (surface: number): string => { - if (self.waveType === "cold") { - // Escala de azul claro a azul oscuro para olas de frío - if (surface < 10) return '#d9fff8'; - else if (surface < 20) return '#bbdfe1'; - else if (surface < 30) return '#9dbeca'; - else if (surface < 40) return '#7f9eb3'; - else if (surface < 50) return '#627e9d'; - else if (surface < 60) return '#445e86'; - else if (surface < 70) return '#263d6f'; - else if (surface < 80) return '#1a2d58'; - else if (surface < 90) return '#112542'; - else return '#081D58'; - } else { - // Escala de rojo claro a rojo burdeos para olas de calor - if (surface < 10) return '#ffcccc'; - else if (surface < 20) return '#ff9999'; - else if (surface < 30) return '#ff6666'; - else if (surface < 40) return '#ff3333'; - else if (surface < 50) return '#ff0000'; - else if (surface < 60) return '#cc0000'; - else if (surface < 70) return '#990000'; - else if (surface < 80) return '#660000'; - else if (surface < 90) return '#4d0000'; - else return '#330000'; - } - }; - this.currentGraph.updateOptions({ - drawPoints: false, - strokeWidth: 0, - fillGraph: false, - visibility: [true, false, false, false], // Mostrar extreme, ocultar surface, duration y event - showRangeSelector: false, - - // CÓDIGO COMENTADO - Configuración para línea con área coloreada - // plotter: null, // Restaurar el plotter por defecto (líneas) - // strokeWidth: 2, - // fillGraph: true, - // fillAlpha: 0.3, - - // Plotter para puntos coloreados por superficie con sombreado de fondo - plotter: function(e: any) { - if (e.setName !== 'extreme') return; - - const ctx = e.drawingContext; - const points = e.points; - const radius = 3; - - ctx.save(); - - // Primero, dibujar el sombreado de fondo para las fechas con ola - // Color según el tipo: amarillo para calor, gris claro para frío - ctx.fillStyle = self.waveType === "cold" ? 'rgba(200, 200, 200, 0.2)' : 'rgba(255, 255, 0, 0.2)'; - - let inWave = false; - let startX = 0; - - for (let i = 0; i < points.length; i++) { - const extremeValue = e.dygraph.getValue(i, 1); // Columna 1 es extreme - const isWave = extremeValue > 0 || extremeValue < 0; // Detectar si hay ola (valores != 0) - - if (isWave && !inWave) { - // Inicio de ola - startX = i > 0 ? points[i - 1].canvasx + (points[i].canvasx - points[i - 1].canvasx) / 2 : points[i].canvasx; - inWave = true; - } else if (!isWave && inWave) { - // Fin de ola - const endX = i > 0 ? points[i - 1].canvasx + (points[i].canvasx - points[i - 1].canvasx) / 2 : points[i].canvasx; - ctx.fillRect(startX, e.plotArea.y, endX - startX, e.plotArea.h); - inWave = false; - } - } - - // Si terminamos en una ola, dibujar hasta el final - if (inWave && points.length > 0) { - const lastPoint = points[points.length - 1]; - ctx.fillRect(startX, e.plotArea.y, lastPoint.canvasx - startX, e.plotArea.h); - } - - // Luego, dibujar los puntos encima del sombreado - for (let i = 0; i < points.length; i++) { - const point = points[i]; - const surfaceValue = e.dygraph.getValue(i, 2); - const pointColor = getColorBySurface(surfaceValue); - - ctx.beginPath(); - ctx.fillStyle = pointColor; - ctx.arc(point.canvasx, point.canvasy, radius, 0, 2 * Math.PI, false); - ctx.fill(); - } - - ctx.restore(); - }, - - series: { - 'extreme': { - color: '#ff6b6b', - strokeWidth: 0, - fillGraph: false, - drawPoints: false - } - }, - axes: { - x: { - valueFormatter: function (millis: number) { - let fecha = new Date(millis); - return self.formatDate(fecha); - }, - axisLabelFormatter: function(number: number) { - const fecha = new Date(number); - if (fecha.getDate() === 1 || fecha.getDate() === 1) { - return self.parent.getMonthName(fecha.getMonth(), true); - } - return ''; - }, - pixelsPerLabel: 50 - } - } - }); - } - - private showFullSeriesViewBar(): void { - if (!this.currentGraph || !this.fullData) return; - - const self = this; - - // Mostrar todos los datos con reconstrucción completa - this.currentGraph.updateOptions({ - file: this.fullData, - showRangeSelector: true, - plotter: function (e: any) { - let ctx = e.drawingContext; - let area = e.plotArea; - - // Configurar estilo de las barras - ctx.fillStyle = "#4285F4"; - ctx.strokeStyle = "#1a73e8"; - ctx.lineWidth = 1; - - // Calcular ancho de cada barra - let barWidth = Math.max(1, (area.w / e.points.length) * 0.8); - - // Dibujar las barras - for (let i = 0; i < e.points.length; i++) { - let point = e.points[i]; - if (!isNaN(point.yval) && point.yval !== null) { - let barHeight = Math.abs(point.canvasy - area.y - area.h); - let barX = point.canvasx - barWidth / 2; - let barY = Math.min(point.canvasy, area.y + area.h); - - ctx.fillRect(barX, barY, barWidth, barHeight); - ctx.strokeRect(barX, barY, barWidth, barHeight); - } - } - }, - axes: { - x: { - valueFormatter: function (millis: any) { - const fecha = new Date(millis); - return fecha.getFullYear().toString(); - }, - axisLabelFormatter: function(number: any) { - const fecha = new Date(number); - return fecha.getFullYear().toString(); - }, - pixelsPerLabel: 80 - }, - y: { - valueFormatter: function (millis: any) { - return " " + (millis < 0.01? millis.toFixed(3) : millis.toFixed(2)); - } - } - } - }); - } - - private showFullSeriesView(): void { - if (!this.currentGraph || !this.fullData) return; - - // Usar todos los datos sin agregar (todos los días de todos los años) - const fullSeriesData = this.fullData; - - const self = this; - - // Función para obtener color según superficie afectada (0-100) y tipo de ola - const getColorBySurface = (surface: number): string => { - if (self.waveType === "cold") { - // Escala de azul claro a azul oscuro para olas de frío - if (surface < 10) return '#d9fff8'; - else if (surface < 20) return '#bbdfe1'; - else if (surface < 30) return '#9dbeca'; - else if (surface < 40) return '#7f9eb3'; - else if (surface < 50) return '#627e9d'; - else if (surface < 60) return '#445e86'; - else if (surface < 70) return '#263d6f'; - else if (surface < 80) return '#1a2d58'; - else if (surface < 90) return '#112542'; - else return '#081D58'; - } else { - // Escala de rojo claro a rojo burdeos para olas de calor - if (surface < 10) return '#ffcccc'; - else if (surface < 20) return '#ff9999'; - else if (surface < 30) return '#ff6666'; - else if (surface < 40) return '#ff3333'; - else if (surface < 50) return '#ff0000'; - else if (surface < 60) return '#cc0000'; - else if (surface < 70) return '#990000'; - else if (surface < 80) return '#660000'; - else if (surface < 90) return '#4d0000'; - else return '#330000'; - } - }; - - // Actualizar gráfico con vista completa - this.currentGraph.updateOptions({ - file: fullSeriesData, - drawPoints: false, // Desactivar puntos por defecto, usaremos plotter personalizado - pointSize: 3, - highlightCircleSize: 5, - strokeWidth: 0, // Sin línea, solo puntos - fillGraph: false, - fillAlpha: 0, - visibility: [true, false, false, false], // Mostrar extreme, ocultar surface, duration y event - showRangeSelector: true, - series: { - 'extreme': { - fillGraph: false, - strokeWidth: 0, - drawPoints: false - } - }, - plotter: function(e: any) { - // Solo dibujar para la serie 'extreme' - if (e.setName !== 'extreme') return; - - const ctx = e.drawingContext; - const points = e.points; - const radius = 3; - - ctx.save(); - - for (let i = 0; i < points.length; i++) { - const point = points[i]; - - // Obtener el valor de superficie (columna 2) para este punto - const surfaceValue = e.dygraph.getValue(i, 2); - - // Obtener el color según la superficie - const pointColor = getColorBySurface(surfaceValue); - - // Dibujar el punto - ctx.beginPath(); - ctx.fillStyle = pointColor; - ctx.arc(point.canvasx, point.canvasy, radius, 0, 2 * Math.PI, false); - ctx.fill(); - } - - ctx.restore(); - }, - axes: { - x: { - valueFormatter: (millis: number) => { - const fecha = new Date(millis); - return fecha.getFullYear().toString(); - }, - axisLabelFormatter: (number: number) => { - const fecha = new Date(number); - return fecha.getFullYear().toString(); - }, - pixelsPerLabel: 80 - } - } - }); - } - - public drawStackedBarGraph(url: any, latlng: CsLatLong): Dygraph { let self = this; @@ -1076,9 +731,9 @@ public showGraph(data: any, latlng: CsLatLong = { lat: 0.0, lng: 0.0 }, station: labelsDiv: document.getElementById('labels'), digitsAfterDecimal: 3, delimiter: ";", - title: this.graphTitle + ' ' + latlng.lat.toFixed(2) + ' N , ' + latlng.lng.toFixed(2) + ' E', - ylabel: this.parent.getState().legendTitle, - xlabel: dateText, + title: this.graphTitle + this.graphSubTitle, + ylabel: this.yLabel || this.parent.getState().legendTitle, + xlabel: this.xLabel || dateText, showRangeSelector: true, plotter: function (e: any) { let ctx = e.drawingContext; From 6062df167a3abd3accce751b0f52d7c2e2a28a33 Mon Sep 17 00:00:00 2001 From: MartaEY Date: Wed, 17 Dec 2025 10:36:16 +0100 Subject: [PATCH 13/28] fix conflict --- anemui-core/src/OpenLayersMap.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/anemui-core/src/OpenLayersMap.ts b/anemui-core/src/OpenLayersMap.ts index 0a93429..74c76df 100644 --- a/anemui-core/src/OpenLayersMap.ts +++ b/anemui-core/src/OpenLayersMap.ts @@ -1353,4 +1353,4 @@ public showFeatureValue(data: any, feature: any, pixel: any, pos: CsLatLong): vo // loadThreshold?: number; // source: VectorSource; // geoJSONData: any; // Your GeoJSON data -// } +// } \ No newline at end of file From edc87880cf64454d04da21c0fbf05eac8c37dd71 Mon Sep 17 00:00:00 2001 From: ManuelPruebas Date: Wed, 17 Dec 2025 12:36:24 +0100 Subject: [PATCH 14/28] =?UTF-8?q?DateFrame:=20Integraci=C3=B3n=20de=20trad?= =?UTF-8?q?ucciones=20en=20la=20clase=20language?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- anemui-core/src/language/language.ts | 95 +++++++--------------------- anemui-core/src/ui/DateFrame.tsx | 91 ++++++++++++-------------- 2 files changed, 64 insertions(+), 122 deletions(-) diff --git a/anemui-core/src/language/language.ts b/anemui-core/src/language/language.ts index 845b40d..43d1d44 100644 --- a/anemui-core/src/language/language.ts +++ b/anemui-core/src/language/language.ts @@ -65,40 +65,16 @@ export default class Language { 'duracion_ola_frio':'Cold wave duration', 'duracion_ola_calor':'Heat wave duration', 'precipitacion_24':'Precipitation in 24 hours', - 'month': { - 1: "January", - 2: "February", - 3: "March", - 4: "April", - 5: "May", - 6: "June", - 7: "July", - 8: "August", - 9: "September", - 10: "October", - 11: "November", - 12: "December" - }, - 'month_short': { - 0: "Jan", - 1: "Feb", - 2: "Mar", - 3: "Apr", - 4: "May", - 5: "Jun", - 6: "Jul", - 7: "Aug", - 8: "Sep", - 9: "Oct", - 10: "Nov", - 11: "Dec" - }, - 'season': { - 1: 'Dec - Feb', - 2: 'Mar - May', - 3: 'Jun - Aug', - 4:' Sep - Nov' - } + 'season': ['Dec - Feb', 'Mar - May', 'Jun - Aug', 'Sep - Nov'], + 'days': ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"], + 'daysShort': ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"], + 'daysMin': ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"], + 'months': ["January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December"], + 'monthsShort': ["Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], + 'today': "Today", + 'clear': "Clear" }, es: { "descargar_nc": "Descargar Archivo", @@ -153,40 +129,16 @@ export default class Language { 'duracion_ola_frio':'Duración de la ola de frio', 'duracion_ola_calor':'Duración de la ola de calor', 'precipitacion_24':'Precipitación en 24h', - 'month': { - 0: "Enero", - 1: "Febrero", - 2: "Marzo", - 3: "Abril", - 4: "Mayo", - 5: "Junio", - 6: "Julio", - 7: "Agosto", - 8: "Septiembre", - 9: "Octubre", - 10: "Noviembre", - 11: "Diciembre" - }, - 'month_short': { - 0: "Ene", - 1: "Feb", - 2: "Mar", - 3: "Abr", - 4: "May", - 5: "Jun", - 6: "Jul", - 7: "Ago", - 8: "Sep", - 9: "Oct", - 10: "Nov", - 11: "Dic" - }, - 'season': { - 1: 'Dic - Feb', - 2: 'Mar - May', - 3: 'Jun - Aug', - 4:' Sep - Nov' - } + 'season': ['Dic - Feb', 'Mar - May', 'Jun - Aug', 'Sep - Nov'], + 'days': ["Domingo", "Lunes", "Martes", "Miércoles", "Jueves", "Viernes", "Sábado"], + 'daysShort': ["Dom", "Lun", "Mar", "Mié", "Jue", "Vie", "Sáb"], + 'daysMin': ["Do", "Lu", "Ma", "Mi", "Ju", "Vi", "Sá"], + 'months': ["Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", + "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"], + 'monthsShort': ["Ene", "Feb", "Mar", "Abr", "May", "Jun", + "Jul", "Ago", "Sep", "Oct", "Nov", "Dic"], + 'today': "Hoy", + 'clear': "Borrar" } } @@ -215,9 +167,10 @@ export default class Language { } public getMonthName(monthIndex: number, short: boolean = false): string { - const key = short ? 'month_short' : 'month'; - if (this.locales[this.defaultLocale][key] && this.locales[this.defaultLocale][key][monthIndex] !== undefined) { - return this.locales[this.defaultLocale][key][monthIndex]; + const key = short ? 'monthsShort' : 'months'; + const monthsArray = this.locales[this.defaultLocale][key]; + if (monthsArray && Array.isArray(monthsArray) && monthsArray[monthIndex] !== undefined) { + return monthsArray[monthIndex]; } return monthIndex.toString(); } diff --git a/anemui-core/src/ui/DateFrame.tsx b/anemui-core/src/ui/DateFrame.tsx index a0be6db..154da90 100644 --- a/anemui-core/src/ui/DateFrame.tsx +++ b/anemui-core/src/ui/DateFrame.tsx @@ -70,7 +70,6 @@ export class DateSelectorFrame extends BaseFrame { super(_parent) this.listener = _listener; let self = this - this.configureDatepickerLocale(); } /** @@ -87,30 +86,26 @@ export class DateSelectorFrame extends BaseFrame { if (locale === 'es' && datepicker && datepicker.dates) { // Configuración española personalizada datepicker.dates['es'] = { - days: ["Domingo", "Lunes", "Martes", "Miércoles", "Jueves", "Viernes", "Sábado"], - daysShort: ["Dom", "Lun", "Mar", "Mié", "Jue", "Vie", "Sáb"], - daysMin: ["Do", "Lu", "Ma", "Mi", "Ju", "Vi", "Sá"], - months: ["Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", - "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"], - monthsShort: ["Ene", "Feb", "Mar", "Abr", "May", "Jun", - "Jul", "Ago", "Sep", "Oct", "Nov", "Dic"], - today: "Hoy", - clear: "Borrar", + days: this.parent.getTranslation('days'), + daysShort: this.parent.getTranslation('daysShort'), + daysMin: this.parent.getTranslation('daysMin'), + months: this.parent.getTranslation('months'), + monthsShort: this.parent.getTranslation('monthsShort'), + today: this.parent.getTranslation('today'), + clear: this.parent.getTranslation('clear'), titleFormat: "MM yyyy", weekStart: 1 }; } else if (locale === 'en' && datepicker && datepicker.dates) { // Configuración inglesa (bootstrap-datepicker tiene 'en' por defecto, pero lo aseguramos) datepicker.dates['en'] = { - days: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"], - daysShort: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"], - daysMin: ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"], - months: ["January", "February", "March", "April", "May", "June", - "July", "August", "September", "October", "November", "December"], - monthsShort: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", - "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], - today: "Today", - clear: "Clear", + days: this.parent.getTranslation('days'), + daysShort: this.parent.getTranslation('daysShort'), + daysMin: this.parent.getTranslation('daysMin'), + months: this.parent.getTranslation('months'), + monthsShort: this.parent.getTranslation('monthsShort'), + today: this.parent.getTranslation('today'), + clear: this.parent.getTranslation('clear'), titleFormat: "MM yyyy", weekStart: 0 }; @@ -150,22 +145,6 @@ export class DateSelectorFrame extends BaseFrame { return dateStr; } - /** - * Convierte una fecha del formato de visualización al formato interno yyyy-mm-dd - */ - private parseDateFromDisplay(dateStr: string): string { - if (!dateStr || locale === 'en') { - return dateStr; // En inglés no hay conversión necesaria - } - - // Convertir dd/mm/yyyy a yyyy-mm-dd para español - const parts = dateStr.split('/'); - if (parts.length === 3) { - return `${parts[2]}-${parts[1]}-${parts[0]}`; - } - return dateStr; - } - public setValidDates(_dates: string[], _varChanged: boolean = false): void { // Handle the case where _dates is actually a single string (annual data) const times = this.parent.getState().times; @@ -422,36 +401,36 @@ export class DateSelectorFrame extends BaseFrame { }; private getSeason(season: string): string { - const translations: any = this.parent.getTranslation('season'); - + const translations: any = this.parent.getTranslation('season'); + switch (season.trim()) { - case translations[2]: + case translations[1]: return '04' - case translations[3]: - return '07' - case translations[4]: + case translations[2]: + return '07' + case translations[3]: return '10' default: - return '01' + return '01' } } public setSeason (seasonId: string) { - const translations: any = this.parent.getTranslation('season'); + const translations: any = this.parent.getTranslation('season'); let season: string; switch (seasonId) { case '04': - season = translations[2]; + season = translations[1]; break; case '07': - season = translations[3]; - break; + season = translations[2]; + break; case '10': - season = translations[4]; + season = translations[3]; break; default: - season = translations[1]; - break; + season = translations[0]; + break; } this.season.config(true, season); } @@ -649,6 +628,9 @@ export class DateSelectorFrame extends BaseFrame { this.sliderFrame = document.getElementById("sliderFrame") as HTMLElement; this.periods = [1,4,12] + // Configurar locale del datepicker ahora que parent está completamente inicializado + this.configureDatepickerLocale(); + let self = this this.updateMode(); this.updatePicker(); @@ -815,8 +797,15 @@ export class DateSelectorFrame extends BaseFrame { public getPeriods(): string[] { const timeSpan = this.getTimeSpan() - let period = timeSpan==2? this.parent.getTranslation('season'):this.parent.getTranslation('month') - return Object.values(period); + if (timeSpan == 2) { + // Para estaciones, devolver los valores del objeto season + const season = this.parent.getTranslation('season'); + return Object.values(season); + } else { + // Para meses, devolver directamente el array de meses + const months = this.parent.getTranslation('months'); + return Array.isArray(months) ? months : Object.values(months); + } } public getTime (time:string[]): number { From 8ba238d6d722996ba4a3a2cb7151f3e642897a86 Mon Sep 17 00:00:00 2001 From: Daniel Vilas Date: Thu, 18 Dec 2025 09:43:24 +0100 Subject: [PATCH 15/28] Versionado 0.2.1 --- version.dev | 2 +- version.main | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/version.dev b/version.dev index b8e2ce4..207cde9 100644 --- a/version.dev +++ b/version.dev @@ -1 +1 @@ -0.2.1-SNAPSHOT +0.2.2-SNAPSHOT diff --git a/version.main b/version.main index 0ea3a94..0c62199 100644 --- a/version.main +++ b/version.main @@ -1 +1 @@ -0.2.0 +0.2.1 From d3b425f543cd5581f1fb441271823c359c1276ee Mon Sep 17 00:00:00 2001 From: Daniel Vilas Date: Thu, 18 Dec 2025 14:31:52 +0100 Subject: [PATCH 16/28] Version para cs-dev --- anemui-core/package.json | 2 +- anemui-demo/package.json | 2 +- anemui-test/package.json | 2 +- package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/anemui-core/package.json b/anemui-core/package.json index ed47290..e52f743 100644 --- a/anemui-core/package.json +++ b/anemui-core/package.json @@ -1,6 +1,6 @@ { "name": "@lcsc/anemui-core", - "version": "0.2.1-SNAPSHOT", + "version": "0.2.2-SNAPSHOT", "description": "Climatic Services Viewer Core", "main": "./src/index.ts", "types": "./src/index.ts", diff --git a/anemui-demo/package.json b/anemui-demo/package.json index 3013640..0d3ee2e 100644 --- a/anemui-demo/package.json +++ b/anemui-demo/package.json @@ -1,6 +1,6 @@ { "name": "@lcsc/anemui-demo", - "version": "0.2.1-SNAPSHOT", + "version": "0.2.2-SNAPSHOT", "description": "Reference Evapotranspiration Monitor", "main": "src/index.ts", "devDependencies": {}, diff --git a/anemui-test/package.json b/anemui-test/package.json index 76e9f93..37cc312 100644 --- a/anemui-test/package.json +++ b/anemui-test/package.json @@ -1,6 +1,6 @@ { "name": "@lcsc/anemui-test", - "version": "0.2.1-SNAPSHOT", + "version": "0.2.2-SNAPSHOT", "description": "Climatic Services Test Suite", "main": "src/index.ts", "devDependencies": {}, diff --git a/package.json b/package.json index 98616d0..b499ad5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@lcsc/anemui-parent", - "version": "0.2.1-SNAPSHOT", + "version": "0.2.2-SNAPSHOT", "description": "Climatic Services Viewer", "scripts": { "test": "mocha --timeout 10000", From 9f5eda13b247c534a84c4c1ffde54237ddea8266 Mon Sep 17 00:00:00 2001 From: ManuelPruebas Date: Mon, 22 Dec 2025 12:30:57 +0100 Subject: [PATCH 17/28] =?UTF-8?q?Graph.tsx:=20Refactorizaci=C3=B3n=20leyen?= =?UTF-8?q?da=20inferior?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- anemui-core/src/index.ts | 2 +- anemui-core/src/ui/Graph.tsx | 75 +++++++++++++++++++++++++++++------- 2 files changed, 62 insertions(+), 15 deletions(-) diff --git a/anemui-core/src/index.ts b/anemui-core/src/index.ts index e077c36..47831ac 100644 --- a/anemui-core/src/index.ts +++ b/anemui-core/src/index.ts @@ -15,7 +15,7 @@ export { hasSubVars } from './Env'; export {enableRenderer } from './tiles/Support'; // public APIs -export { CsGraph, type GraphType } from './ui/Graph'; +export { CsGraph, type GraphType, ColorLegendConfig } from './ui/Graph'; export { MenuBar, MenuBarListener } from './ui/MenuBar'; export { DateFrameMode } from "./ui/DateFrame"; diff --git a/anemui-core/src/ui/Graph.tsx b/anemui-core/src/ui/Graph.tsx index 0b6ffa9..113f594 100644 --- a/anemui-core/src/ui/Graph.tsx +++ b/anemui-core/src/ui/Graph.tsx @@ -25,6 +25,18 @@ interface MeanLineConfig { labelPadding: number; } +export interface ColorLegendRange { + min: number; + max: number; + color: string; + label: string; +} + +export interface ColorLegendConfig { + title: string; + ranges: ColorLegendRange[]; +} + export class CsGraph extends BaseFrame { private graphTitle: string; @@ -108,19 +120,7 @@ export class CsGraph extends BaseFrame {
    -
    - Superficie afectada: -
    0-10%
    -
    10-20%
    -
    20-30%
    -
    30-40%
    -
    40-50%
    -
    50-60%
    -
    60-70%
    -
    70-80%
    -
    80-90%
    -
    90-100%
    -
    +