From 633a442ffa854c2324a5f9eb70c523db612019af Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Fri, 5 Jun 2026 14:35:07 +0200 Subject: [PATCH 01/25] feat(lib): detect the grid type from the zarr dggs convention --- src/lib/data/ZarrDataManager.ts | 30 +++++++++++++++++++++ src/lib/data/gridTypeDetector.ts | 35 ++++++++++++++++++++++++ src/ui/grids/Healpix.vue | 46 ++++++++++++++++++++++++++------ 3 files changed, 103 insertions(+), 8 deletions(-) diff --git a/src/lib/data/ZarrDataManager.ts b/src/lib/data/ZarrDataManager.ts index 166be1e..caa4d37 100644 --- a/src/lib/data/ZarrDataManager.ts +++ b/src/lib/data/ZarrDataManager.ts @@ -150,6 +150,28 @@ export class ZarrDataManager { return array; } + static async getParentGroup( + datasources: TSources, + varname: string, + format?: TZarrFormat + ): Promise> { + const groupPath = await ZarrDataManager.resolveGroupPath(varname); + const source = ZarrDataManager.getDatasetSource(datasources, varname); + const target = (await ZarrDataManager.getDataset(source)).resolve( + groupPath + ); + + let dataset: zarr.Group; + if (format === ZARR_FORMAT.V2) { + dataset = await zarr.open.v2(target, { kind: "group" }); + } else if (format === ZARR_FORMAT.V3) { + dataset = await zarr.open.v3(target, { kind: "group" }); + } else { + dataset = await zarr.open(target, { kind: "group" }); + } + return dataset; + } + static async getVariableInfoByDatasetSources( datasource: TSources, variable: string @@ -235,6 +257,14 @@ export class ZarrDataManager { return datavar.dimensionNames ?? []; } + static resolveGroupPath(variable: string): string { + if (!variable.includes("/")) { + return "/"; + } + + return variable.split("/").slice(0, -1).join("/"); + } + static resolveVariablePath( contextVariable: string, variable: string diff --git a/src/lib/data/gridTypeDetector.ts b/src/lib/data/gridTypeDetector.ts index d27b401..31b117a 100644 --- a/src/lib/data/gridTypeDetector.ts +++ b/src/lib/data/gridTypeDetector.ts @@ -146,6 +146,32 @@ async function determineGridTypeFromCRS( return null; } +function determineGridTypeFromDGGSZarrConvention(metadata: any) { + if (metadata["name"] !== "healpix") { + // unsupported DGGS, for now + return GRID_TYPES.ERROR; + } + + return GRID_TYPES.HEALPIX; +} + +async function determineGridTypeFromZarrConvention( + datasources: TSources, + varnameSelector: string +): Promise { + const group = await ZarrDataManager.getParentGroup( + datasources, + varnameSelector, + datasources?.zarr_format + ); + const metadata = group.attrs; + + if (metadata["dggs"] !== undefined) { + return determineGridTypeFromDGGSZarrConvention(metadata["dggs"]); + } + return null; +} + // Determine grid type from lat/lon data analysis async function determineGridTypeFromData( variable: string, @@ -214,6 +240,15 @@ export async function getGridType( return crsGridType; } + // zarr convention metadata + const zarrConventionType = await determineGridTypeFromZarrConvention( + datasources!, + varnameSelector + ); + if (zarrConventionType) { + return zarrConventionType; + } + const dimensions = await ZarrDataManager.getDimensionNames( datasources!, varnameSelector diff --git a/src/ui/grids/Healpix.vue b/src/ui/grids/Healpix.vue index 3f4a83a..d029d0a 100644 --- a/src/ui/grids/Healpix.vue +++ b/src/ui/grids/Healpix.vue @@ -163,16 +163,46 @@ function fetchGrid() { } async function getNside() { - const crs = await ZarrDataManager.getCRSInfo( - props.datasources!, - varnameSelector.value - ); - // FIXME: could probably have other names - const nside = crs.attrs["healpix_nside"] as number; - return nside; + try { + const crs = await ZarrDataManager.getCRSInfo( + props.datasources!, + varnameSelector.value + ); + + return crs.attrs["healpix_nside"] as number; + // FIXME: could probably have other names + } catch (error) { + const group = await ZarrDataManager.getParentGroup( + props.datasources!, + varnameSelector.value + ); + const metadata: any = group.attrs?.dggs ?? {}; + if ("refinement_level" in metadata) { + const refinementLevel = (metadata.refinement_level ?? 0) as number; + return Math.pow(2, refinementLevel); + } + + throw error; + } } async function getCells() { + var cellCoord = "cell"; + try { + const group = await ZarrDataManager.getParentGroup( + props.datasources!, + varnameSelector.value + ); + const metadata: any = group.attrs?.dggs ?? {}; + + const key = "coordinate"; + if (key in metadata) { + cellCoord = metadata[key]; + } + } catch { + // no dggs metadata found, continue with the default cell coordinate + } + try { const rawCells = ( await ZarrDataManager.getVariableData( @@ -180,7 +210,7 @@ async function getCells() { props.datasources!, varnameSelector.value ), - ZarrDataManager.resolveVariablePath(varnameSelector.value, "cell") + ZarrDataManager.resolveVariablePath(varnameSelector.value, cellCoord) ) ).data as ArrayLike; From 2cf5790ee8de2bd1311118f70a958ec13cb3e369 Mon Sep 17 00:00:00 2001 From: Andrej Fast Date: Tue, 9 Jun 2026 11:35:28 +0200 Subject: [PATCH 02/25] fix(lib): fixed EPSG3857 if spatial_ref is a coordinate --- package-lock.json | 37 +++++++++++++++++++ package.json | 1 + src/lib/data/coordinateVariables.ts | 57 ++++++++++++++++++++++++----- src/ui/grids/Regular.vue | 25 +++++-------- 4 files changed, 96 insertions(+), 24 deletions(-) diff --git a/package-lock.json b/package-lock.json index 604630e..40aac75 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "kdbush": "4.0.2", "pinia": "^3.0.4", "primevue": "^4.5.5", + "proj4": "^2.20.9", "qrcode": "^1.5.4", "quick-lru": "^7.3.0", "three": "^0.184", @@ -6703,6 +6704,12 @@ "dev": true, "license": "MIT" }, + "node_modules/mgrs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mgrs/-/mgrs-1.0.0.tgz", + "integrity": "sha512-awNbTOqCxK1DBGjalK3xqWIstBZgN6fxsMSiXLs9/spqWkF2pAhb2rrYCFSsr1/tT7PhcDGjZndG8SWYn0byYA==", + "license": "MIT" + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -7348,6 +7355,27 @@ "dev": true, "license": "MIT" }, + "node_modules/proj4": { + "version": "2.20.9", + "resolved": "https://registry.npmjs.org/proj4/-/proj4-2.20.9.tgz", + "integrity": "sha512-GLBGqXaTcdWnppre3o1sMmy4DcMGSGq/ng+9k2MTNddarRK6SveINqlqYzi3xEXuy06ljY1TTrC6H9C4f360IQ==", + "license": "MIT", + "dependencies": { + "mgrs": "1.0.0", + "wkt-parser": "^1.5.5" + }, + "funding": { + "url": "https://github.com/sponsors/ahocevar" + }, + "peerDependencies": { + "geotiff": "*" + }, + "peerDependenciesMeta": { + "geotiff": { + "optional": true + } + } + }, "node_modules/proxy-from-env": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", @@ -9278,6 +9306,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/wkt-parser": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/wkt-parser/-/wkt-parser-1.5.5.tgz", + "integrity": "sha512-/zMYi94/7D7fxcOSlVmWn6vnOMj3Gq5d1xvVjaYOS9n6h0qOJ4I7YYVxBWYcH1vq9+suhqzXkn05Yx47zQNUIA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ahocevar" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", diff --git a/package.json b/package.json index 300331f..36f1178 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "kdbush": "4.0.2", "pinia": "^3.0.4", "primevue": "^4.5.5", + "proj4": "^2.20.9", "qrcode": "^1.5.4", "quick-lru": "^7.3.0", "three": "^0.184", diff --git a/src/lib/data/coordinateVariables.ts b/src/lib/data/coordinateVariables.ts index ec5ce35..078d1db 100644 --- a/src/lib/data/coordinateVariables.ts +++ b/src/lib/data/coordinateVariables.ts @@ -1,3 +1,4 @@ +import proj4 from "proj4"; import * as zarr from "zarrita"; import { decodeVariableChunkInPlace } from "./variableDecoding.ts"; @@ -5,7 +6,8 @@ import { ZarrDataManager } from "./ZarrDataManager.ts"; import { type TSources } from "@/lib/types/GlobeTypes.ts"; -const EARTH_RADIUS = 6378137; +const WGS84 = "EPSG:4326"; +const WEB_MERCATOR = "EPSG:3857"; export function isWebMercatorCRS(crsWkt: string): boolean { return ( @@ -24,26 +26,63 @@ export function isProjectedYName(name: string): boolean { return name === "y"; } -export function webMercatorToLonLat( +function transformProjectedAxesToLonLat( x: Float64Array, - y: Float64Array + y: Float64Array, + crsWkt: string ): { - longitudes: Float64Array; - latitudes: Float64Array; + longitudes: Float64Array; + latitudes: Float64Array; } { + const transformer = proj4(crsWkt, WGS84); const longitudes = new Float64Array(x.length); const latitudes = new Float64Array(y.length); + for (let i = 0; i < x.length; i++) { - longitudes[i] = (x[i] / EARTH_RADIUS) * (180 / Math.PI); + const [lon] = transformer.forward([x[i], 0]); + longitudes[i] = lon; } for (let i = 0; i < y.length; i++) { - latitudes[i] = - (Math.atan(Math.exp(y[i] / EARTH_RADIUS)) * 2 - Math.PI / 2) * - (180 / Math.PI); + const [, lat] = transformer.forward([0, y[i]]); + latitudes[i] = lat; } + return { longitudes, latitudes }; } +export function webMercatorToLonLat( + x: Float64Array, + y: Float64Array +): { + longitudes: Float64Array; + latitudes: Float64Array; +} { + return transformProjectedAxesToLonLat(x, y, WEB_MERCATOR); +} + +export function projectedAxisCoordinatesToLonLat( + x: Float64Array, + y: Float64Array, + crsWkt: string | null +): { + longitudes: Float64Array; + latitudes: Float64Array; +} { + if (!crsWkt) { + return { + longitudes: new Float64Array(x), + latitudes: new Float64Array(y), + }; + } + if (isWebMercatorCRS(crsWkt)) { + return transformProjectedAxesToLonLat(x, y, crsWkt); + } + return { + longitudes: new Float64Array(x), + latitudes: new Float64Array(y), + }; +} + export async function getCRSWkt( datasources: TSources, variable: string diff --git a/src/ui/grids/Regular.vue b/src/ui/grids/Regular.vue index 31bcd67..5a03532 100644 --- a/src/ui/grids/Regular.vue +++ b/src/ui/grids/Regular.vue @@ -18,8 +18,7 @@ import { isLongitudeName, isProjectedXName, isProjectedYName, - isWebMercatorCRS, - webMercatorToLonLat, + projectedAxisCoordinatesToLonLat, } from "@/lib/data/coordinateVariables.ts"; import { downsampleDataTexture } from "@/lib/data/dataTexture.ts"; import { buildDimensionRangesAndIndices } from "@/lib/data/dimensionHandling.ts"; @@ -83,8 +82,8 @@ const { const { setHoverLookupFromIndex, clearHoverLookup } = useGridHoverLookup(hoveredGeoPoint); -const longitudes = ref(new Float64Array()); -const latitudes = ref(new Float64Array()); +const longitudes = ref(new Float64Array()); +const latitudes = ref(new Float64Array()); const BATCH_SIZE = 60; const MAX_GEO_RESOLUTION = 512; @@ -135,17 +134,13 @@ async function fetchProjectedXYDims( ), ]); const crsWkt = await getCRSWkt(props.datasources!, varnameSelector.value); - if (crsWkt && isWebMercatorCRS(crsWkt)) { - const converted = webMercatorToLonLat( - new Float64Array(xData.data as Float64Array), - new Float64Array(yData.data as Float64Array) - ); - longitudes.value = converted.longitudes; - latitudes.value = converted.latitudes; - } else { - longitudes.value = new Float64Array(xData.data as Float64Array); - latitudes.value = new Float64Array(yData.data as Float64Array); - } + const converted = projectedAxisCoordinatesToLonLat( + new Float64Array(xData.data as Float64Array), + new Float64Array(yData.data as Float64Array), + crsWkt + ); + longitudes.value = converted.longitudes; + latitudes.value = converted.latitudes; } async function getDims() { From 5ceed6d26daaa92bac76c4c14c2aef8cc10b29b4 Mon Sep 17 00:00:00 2001 From: Andrej Fast Date: Tue, 9 Jun 2026 14:00:23 +0200 Subject: [PATCH 03/25] fix(lib): better dataset recognition for Curvilinear and Regular Datasets --- src/lib/data/ZarrDataManager.ts | 7 ++ src/lib/data/coordinateVariables.ts | 176 +++++++++++++++++++++++++++- src/lib/data/gridTypeDetector.ts | 30 ++++- src/ui/grids/Curvilinear.vue | 58 +++++++-- 4 files changed, 256 insertions(+), 15 deletions(-) diff --git a/src/lib/data/ZarrDataManager.ts b/src/lib/data/ZarrDataManager.ts index 166be1e..7e40c3e 100644 --- a/src/lib/data/ZarrDataManager.ts +++ b/src/lib/data/ZarrDataManager.ts @@ -211,6 +211,13 @@ export class ZarrDataManager { if (group.attrs?.grid_mapping) { return String(group.attrs.grid_mapping).split(":")[0]; } + if ( + (datavar.attrs?.coordinates as string | undefined)?.includes( + "spatial_ref" + ) + ) { + return "spatial_ref"; + } return "crs"; } diff --git a/src/lib/data/coordinateVariables.ts b/src/lib/data/coordinateVariables.ts index 078d1db..4247d42 100644 --- a/src/lib/data/coordinateVariables.ts +++ b/src/lib/data/coordinateVariables.ts @@ -9,6 +9,23 @@ import { type TSources } from "@/lib/types/GlobeTypes.ts"; const WGS84 = "EPSG:4326"; const WEB_MERCATOR = "EPSG:3857"; +export const ProjectedCoordinateName = { + X: "x", + Y: "y", +} as const; + +export type TProjectedCoordinateName = + (typeof ProjectedCoordinateName)[keyof typeof ProjectedCoordinateName]; + +export const CrsWktAttributeName = { + CRS_WKT: "crs_wkt", + SPATIAL_REF: "spatial_ref", + PROJECTION: "projection", +} as const; + +export type TCrsWktAttributeName = + (typeof CrsWktAttributeName)[keyof typeof CrsWktAttributeName]; + export function isWebMercatorCRS(crsWkt: string): boolean { return ( crsWkt.includes('AUTHORITY["EPSG","3857"]') || @@ -19,11 +36,11 @@ export function isWebMercatorCRS(crsWkt: string): boolean { } export function isProjectedXName(name: string): boolean { - return name === "x"; + return getVariableLocalName(name) === ProjectedCoordinateName.X; } export function isProjectedYName(name: string): boolean { - return name === "y"; + return getVariableLocalName(name) === ProjectedCoordinateName.Y; } function transformProjectedAxesToLonLat( @@ -83,19 +100,170 @@ export function projectedAxisCoordinatesToLonLat( }; } +function getStringAttribute( + attrs: zarr.Attributes, + name: TCrsWktAttributeName +) { + const value = attrs[name]; + return typeof value === "string" ? value : null; +} + +function getWktFromAttrs(attrs: zarr.Attributes) { + return ( + getStringAttribute(attrs, CrsWktAttributeName.CRS_WKT) ?? + getStringAttribute(attrs, CrsWktAttributeName.SPATIAL_REF) ?? + getStringAttribute(attrs, CrsWktAttributeName.PROJECTION) + ); +} + +async function getGroupCRSWkt(datasources: TSources, variable: string) { + const group = await ZarrDataManager.getDatasetGroup( + ZarrDataManager.getDatasetSource(datasources, variable) + ); + return getWktFromAttrs(group.attrs); +} + export async function getCRSWkt( datasources: TSources, variable: string ): Promise { try { const crs = await ZarrDataManager.getCRSInfo(datasources, variable); - const wkt = crs.attrs["crs_wkt"] ?? crs.attrs["spatial_ref"]; - return typeof wkt === "string" ? wkt : null; + const wkt = getWktFromAttrs(crs.attrs); + if (wkt) { + return wkt; + } + } catch { + // Fall through to group attrs. + } + try { + return await getGroupCRSWkt(datasources, variable); } catch { return null; } } +function createFloat64Chunk( + data: Float64Array, + shape: number[] +): zarr.Chunk<"float64"> { + let strideValue = 1; + const stride = new Array(shape.length); + for (let i = shape.length - 1; i >= 0; i--) { + stride[i] = strideValue; + strideValue *= shape[i]; + } + return { data, shape, stride }; +} + +function toFloat64Array(data: zarr.TypedArray) { + if (data instanceof Float64Array) { + return data; + } + return Float64Array.from(data as ArrayLike); +} + +function projectXYGridToLonLat( + x: Float64Array, + y: Float64Array, + crsWkt: string | null +) { + const shape = [y.length, x.length]; + const longitudes = new Float64Array(x.length * y.length); + const latitudes = new Float64Array(x.length * y.length); + const transformer = crsWkt ? proj4(crsWkt, WGS84) : null; + + for (let j = 0; j < y.length; j++) { + for (let i = 0; i < x.length; i++) { + const index = j * x.length + i; + if (transformer) { + const [lon, lat] = transformer.forward([x[i], y[j]]); + longitudes[index] = lon; + latitudes[index] = lat; + } else { + longitudes[index] = x[i]; + latitudes[index] = y[j]; + } + } + } + + return { + latitudes: createFloat64Chunk(latitudes, shape), + longitudes: createFloat64Chunk(longitudes, shape), + }; +} + +function getProjectedXYNames(dimensionNames: string[]) { + const xName = dimensionNames.find(isProjectedXName); + const yName = dimensionNames.find(isProjectedYName); + if (!xName || !yName) { + throw new Error("Projected x/y dimensions not found"); + } + return { xName, yName }; +} + +async function fetchProjectedXYVariables( + datasources: TSources, + variable: string, + xName: string, + yName: string +) { + const gridsource = datasources.levels[0].grid; + const [xVar, yVar] = await Promise.all([ + ZarrDataManager.getVariableInfo( + gridsource, + ZarrDataManager.resolveVariablePath(variable, xName) + ), + ZarrDataManager.getVariableInfo( + gridsource, + ZarrDataManager.resolveVariablePath(variable, yName) + ), + ]); + return { xVar, yVar }; +} + +export async function getProjectedXYLonLatData( + variable: string, + datavar: zarr.Array, + datasources: TSources | undefined, + dimensionNames = datavar.dimensionNames ?? [] +) { + const { xName, yName } = getProjectedXYNames(dimensionNames); + const { xVar, yVar } = await fetchProjectedXYVariables( + datasources!, + variable, + xName, + yName + ); + const [xCoordinates, yCoordinates] = await Promise.all([ + ZarrDataManager.getVariableDataFromArray(xVar), + ZarrDataManager.getVariableDataFromArray(yVar), + ]); + decodeVariableChunkInPlace(xCoordinates, xVar.attrs); + decodeVariableChunkInPlace(yCoordinates, yVar.attrs); + + const crsWkt = await getCRSWkt(datasources!, variable); + const { latitudes, longitudes } = projectXYGridToLonLat( + toFloat64Array(xCoordinates.data), + toFloat64Array(yCoordinates.data), + crsWkt + ); + const geographicDimensionNames = [yName, xName]; + + return { + latitudesAttrs: { + dimensionNames: geographicDimensionNames, + units: "degrees_north", + }, + latitudes, + longitudesAttrs: { + dimensionNames: geographicDimensionNames, + units: "degrees_east", + }, + longitudes, + }; +} + type TLatLonNames = { latitudeName: string | null; longitudeName: string | null; diff --git a/src/lib/data/gridTypeDetector.ts b/src/lib/data/gridTypeDetector.ts index d27b401..5422d5d 100644 --- a/src/lib/data/gridTypeDetector.ts +++ b/src/lib/data/gridTypeDetector.ts @@ -1,11 +1,13 @@ import * as zarr from "zarrita"; import { + getCRSWkt, getLatLonData, isLatitudeName, isLongitudeName, isProjectedXName, isProjectedYName, + isWebMercatorCRS, } from "./coordinateVariables.ts"; import { ZarrDataManager } from "./ZarrDataManager.ts"; @@ -120,6 +122,25 @@ function checkProjectedXYDimensions(dimensions: string[]): boolean { return dimensions.some(isProjectedXName) && dimensions.some(isProjectedYName); } +async function determineProjectedXYGridType( + datasources: TSources | undefined, + variable: string, + dimensions: string[] +): Promise { + if (!checkProjectedXYDimensions(dimensions)) { + return null; + } + if (!datasources) { + return GRID_TYPES.REGULAR; + } + + const crsWkt = await getCRSWkt(datasources, variable); + if (crsWkt && !isWebMercatorCRS(crsWkt)) { + return GRID_TYPES.CURVILINEAR; + } + return GRID_TYPES.REGULAR; +} + // Attempt to determine grid type from CRS information async function determineGridTypeFromCRS( datasources: TSources, @@ -175,10 +196,11 @@ async function determineGridTypeFromData( // an irregular grid return GRID_TYPES.IRREGULAR; } catch { - if (checkProjectedXYDimensions(dimensions)) { - return GRID_TYPES.REGULAR; - } - return null; + return await determineProjectedXYGridType( + datasources, + variable, + dimensions + ); } } diff --git a/src/ui/grids/Curvilinear.vue b/src/ui/grids/Curvilinear.vue index 3fa650e..c2179bb 100755 --- a/src/ui/grids/Curvilinear.vue +++ b/src/ui/grids/Curvilinear.vue @@ -11,7 +11,12 @@ import { import { useGridDataLoader } from "./composables/useGridDataLoader.ts"; import { useSharedGridLogic } from "./composables/useSharedGridLogic.ts"; -import { getLatLonData } from "@/lib/data/coordinateVariables.ts"; +import { + getLatLonData, + getProjectedXYLonLatData, + isProjectedXName, + isProjectedYName, +} from "@/lib/data/coordinateVariables.ts"; import { buildDimensionRangesAndIndices } from "@/lib/data/dimensionHandling.ts"; import { castDataVarToFloat32, @@ -106,14 +111,37 @@ const { datasourceUpdate } = useGridDataLoader({ const BATCH_SIZE = 30; +async function getCurvilinearCoordinates( + datavar: zarr.Array, + dimensionNames: string[] +) { + try { + return await getLatLonData( + varnameSelector.value, + datavar, + props.datasources + ); + } catch (error) { + if (hasTrailingProjectedXYDimensions(dimensionNames)) { + return await getProjectedXYLonLatData( + varnameSelector.value, + datavar, + props.datasources, + dimensionNames + ); + } + throw error; + } +} + async function getGrid( datavar: zarr.Array, - data: Float32Array + data: Float32Array, + dimensionNames: string[] ) { - const { latitudes, longitudes } = await getLatLonData( - varnameSelector.value, + const { latitudes, longitudes } = await getCurvilinearCoordinates( datavar, - props.datasources + dimensionNames ); const isMissingOrFill = createMissingOrFillPredicate(datavar); @@ -151,6 +179,15 @@ async function getGrid( }; } +function hasTrailingProjectedXYDimensions(dimensionNames: string[]) { + if (dimensionNames.length < 2) { + return false; + } + const lastDim = dimensionNames[dimensionNames.length - 1]; + const secondLastDim = dimensionNames[dimensionNames.length - 2]; + return isProjectedXName(lastDim) && isProjectedYName(secondLastDim); +} + function detectLongitudeFlip( longitudes: Float64Array, latitudes: Float64Array, @@ -782,11 +819,12 @@ function setHoverData( async function renderGridAndHover( datavar: zarr.Array, rawData: Float32Array, + dimensionNames: string[], fillValue: number, missingValue: number ) { const { latitudesData, longitudesData, nj, ni, shouldFlipLongitude } = - await getGrid(datavar, rawData); + await getGrid(datavar, rawData, dimensionNames); setHoverData( rawData, latitudesData, @@ -825,7 +863,13 @@ async function fetchAndRenderData( rawData ); - await renderGridAndHover(datavar, rawData, fillValue, missingValue); + await renderGridAndHover( + datavar, + rawData, + dimensionNames, + fillValue, + missingValue + ); const dimInfo = await getDimensionValues(dimensionRanges, indices); From b1efc238b3c586da9eed3a0e8312477a38fdc63f Mon Sep 17 00:00:00 2001 From: Andrej Fast Date: Tue, 9 Jun 2026 14:13:00 +0200 Subject: [PATCH 04/25] chore(lib): get dimensions in parallel --- src/lib/data/dimensionData.ts | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/src/lib/data/dimensionData.ts b/src/lib/data/dimensionData.ts index 1173ecb..3539476 100644 --- a/src/lib/data/dimensionData.ts +++ b/src/lib/data/dimensionData.ts @@ -104,27 +104,23 @@ export async function fetchDimensionDetails( dimensionRanges: TDimensionRange[], dimSlidersValues: (number | zarr.Slice | null)[] ): Promise { - const array: TDimInfo[] = []; - for (let i = 0; i < dimensionRanges.length; i++) { - const dim = dimensionRanges[i]; - if (dim?.name === "time") { - const timeInfo = await getTimeInfo( - datasources, - dimensionRanges, - i, - dimSlidersValues[i] as number, - currentVariable - ); - array.push(timeInfo); - } else { - const dimInfo = await getDimensionInfo( + return await Promise.all( + dimensionRanges.map((dim, i) => { + if (dim?.name === "time") { + return getTimeInfo( + datasources, + dimensionRanges, + i, + dimSlidersValues[i] as number, + currentVariable + ); + } + return getDimensionInfo( datasources.levels[0].datasources[currentVariable], dim!, dimSlidersValues[i] as number, currentVariable ); - array.push(dimInfo); - } - } - return array; + }) + ); } From 3bb62ac87f0dcf26cf403eebd51ab5c7e8b35d4a Mon Sep 17 00:00:00 2001 From: Andrej Fast Date: Tue, 9 Jun 2026 15:42:49 +0200 Subject: [PATCH 05/25] chore(ui): Control-Panel is no longer open when loading a new dataset on mobile devices --- src/ui/overlays/Controls.vue | 2 +- src/views/GlobeView.vue | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ui/overlays/Controls.vue b/src/ui/overlays/Controls.vue index db8c387..2690f6f 100644 --- a/src/ui/overlays/Controls.vue +++ b/src/ui/overlays/Controls.vue @@ -329,7 +329,7 @@ defineExpose({