Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion web/client/utils/GeoNodeUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import { isImageServerUrl } from './ArcGISUtils';
import { getConfigProp } from './ConfigUtils';
import { getSupportedLocales, shortLocale } from './LocaleUtils';
import uuid from 'uuid';
import { isEmpty } from 'lodash';
import queryString from 'query-string';
Expand Down Expand Up @@ -278,6 +279,31 @@ function getExtentFromResource({ extent }) {
return bbox;
}

const getLocalizedValue = (resource, key, locale = '') => {
if (resource[`${key}_${locale}`]) {
return resource[`${key}_${locale}`];
}
const lang = shortLocale(locale);
if (lang && resource[`${key}_${lang}`]) {
return resource[`${key}_${lang}`];
}
return null;
};

const getLocalizedValues = (resource, key, defaultValue) => {
const supportedLocales = getSupportedLocales() || {};
const translations = Object.values(supportedLocales)
.map(({ code }) => {
const value = getLocalizedValue(resource, key, code);
return value ? [code, value] : null;
})
.filter(value => value !== null);
if (translations.length) {
return { ...Object.fromEntries(translations), 'default': defaultValue };
}
return defaultValue;
};

/**
* convert resource layer configuration to a mapstore layer object
* @param {object} resource geonode layer resource
Expand All @@ -289,7 +315,7 @@ export const resourceToLayerConfig = (resource, options) => {
alternate,
links = [],
featureinfo_custom_template: template,
title,
title: defaultTitle,
perms,
pk,
default_style: defaultStyle,
Expand All @@ -301,6 +327,8 @@ export const resourceToLayerConfig = (resource, options) => {

const layerSettings = data?.layerSettings ?? data;

const title = getLocalizedValues(resource, 'title', defaultTitle);

const bbox = getExtentFromResource(resource);
const defaultStyleParams = defaultStyle && {
defaultStyle: {
Expand Down
33 changes: 33 additions & 0 deletions web/client/utils/LocaleUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,39 @@ export const getErrorMessage = (e, service, section) => {
*/
export const getLocalizedProp = (locale, prop) => isObject(prop) ? prop[locale] || prop.default : prop || '';

/**
* Returns the normalized locale code in language-region format (e.g. 'en' → 'en-US', 'en-GB' → 'en-GB').
* Returns an empty string if the code is invalid or missing.
* @param {string} code locale code
* @returns {string}
*/
export const longLocale = (code) => {
if (!code) return '';
try {
const loc = new Intl.Locale(code);
if (loc.region) return `${loc.language}-${loc.region}`;
const maximized = loc.maximize();
return `${maximized.language}-${maximized.region}`;
} catch {
return '';
}
};

/**
* Returns the language component of a locale code (e.g. 'en-US' → 'en').
* Returns an empty string if the code is invalid or missing.
* @param {string} code locale code
* @returns {string}
*/
export const shortLocale = (code) => {
if (!code) return '';
try {
return new Intl.Locale(code).language;
} catch {
return '';
}
};

LocaleUtils = {
getLocale,
normalizeLocaleCode
Expand Down
237 changes: 237 additions & 0 deletions web/client/utils/__tests__/GeoNodeUtils-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
/*
* Copyright 2025, GeoSolutions Sas.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/

import expect from 'expect';

import { setSupportedLocales, getSupportedLocales } from '../LocaleUtils';
import { resourceToLayerConfig, getDimensions } from '../GeoNodeUtils';

describe('GeoNodeUtils', () => {
describe('resourceToLayerConfig', () => {

let originalLocales;
beforeEach(() => {
originalLocales = getSupportedLocales();
});
afterEach(() => {
setSupportedLocales(originalLocales);
});

it('should keep the wms params from the url if available', () => {
const newLayer = resourceToLayerConfig({
alternate: 'geonode:layer_name',
links: [{
extension: 'html',
link_type: 'OGC:WMS',
name: 'OGC WMS Service',
mime: 'text/html',
url: 'http://localhost:8080/geoserver/wms?map=name&map_resolution=91'
}],
title: 'Layer title',
perms: [],
pk: 1
});
expect(newLayer.params).toEqual({ map: 'name', map_resolution: '91' });
});

it('should apply layer settings from dataset data', () => {
const newLayer = resourceToLayerConfig({
alternate: 'geonode:layer_name',
links: [{
extension: 'html',
link_type: 'OGC:WMS',
name: 'OGC WMS Service',
mime: 'text/html',
url: 'http://localhost:8080/geoserver/wms?map=name&map_resolution=91'
}],
title: 'Layer title',
perms: [],
pk: 1,
data: {opacity: 0.8}
});
expect(newLayer.opacity).toBe(0.8);
});

it('should parse arcgis dataset', () => {
const newLayer = resourceToLayerConfig({
alternate: 'remoteWorkspace:1',
title: 'Layer title',
perms: [],
links: [{
extension: 'html',
link_type: 'image',
mime: 'text/html',
name: 'ArcGIS REST ImageServer',
url: 'http://localhost:8080/MapServer'
}],
pk: 1,
ptype: 'gxp_arcrestsource'
});
expect(newLayer.type).toBe('arcgis');
expect(newLayer.name).toBe('1');
expect(newLayer.url).toBe('http://localhost:8080/MapServer');
});

it('should return localized title object when supported locales have translations', () => {
setSupportedLocales({
'en': { code: 'en-US', description: 'English' },
'it': { code: 'it-IT', description: 'Italiano' },
'fr': { code: 'fr-FR', description: 'Français' }
});
const newLayer = resourceToLayerConfig({
alternate: 'geonode:layer_numtilangue',
title: 'Default title',
title_en: 'Layer title',
title_it: 'Titolo del layer',
title_fr: 'Titre de la couche',
perms: [],
pk: 1
});
expect(newLayer.title).toEqual({
'en-US': 'Layer title',
'it-IT': 'Titolo del layer',
'fr-FR': 'Titre de la couche',
'default': 'Default title'
});
});

it('should return plain title string when no locale translations exist', () => {
setSupportedLocales({});
const newLayer = resourceToLayerConfig({
alternate: 'geonode:layer_name',
title: 'Plain title',
links: [{ link_type: 'OGC:WMS', url: '/geoserver/wms' }],
perms: [],
pk: 1
});
expect(newLayer.title).toBe('Plain title');
});

describe('alternate in extendedParams', () => {
it('WMS layer includes alternate in extendedParams', () => {
const newLayer = resourceToLayerConfig({
alternate: 'geonode:layer_name',
links: [{
extension: 'html',
link_type: 'OGC:WMS',
name: 'OGC WMS Service',
mime: 'text/html',
url: '/geoserver/wms'
}],
title: 'Layer title',
perms: [],
pk: 1
});
expect(newLayer.extendedParams).toEqual({ pk: 1, alternate: 'geonode:layer_name' });
});

it('3dtiles layer includes alternate in extendedParams', () => {
const newLayer = resourceToLayerConfig({
alternate: 'geonode:tileset',
subtype: '3dtiles',
links: [{ extension: '3dtiles', url: '/tileset.json' }],
title: 'Tileset',
perms: [],
pk: 2
});
expect(newLayer.extendedParams).toEqual({ pk: 2, alternate: 'geonode:tileset' });
});

it('cog layer includes alternate in extendedParams', () => {
const newLayer = resourceToLayerConfig({
alternate: 'geonode:cog_layer',
subtype: 'cog',
links: [{ extension: 'cog', url: '/raster.tif' }],
title: 'COG',
perms: [],
pk: 3
});
expect(newLayer.extendedParams).toEqual({ pk: 3, alternate: 'geonode:cog_layer' });
});

it('flatgeobuf layer includes alternate in extendedParams', () => {
const newLayer = resourceToLayerConfig({
alternate: 'geonode:fgb_layer',
subtype: 'flatgeobuf',
links: [{ extension: 'flatgeobuf', url: '/data.fgb' }],
title: 'FGB',
perms: [],
pk: 4
});
expect(newLayer.extendedParams).toEqual({ pk: 4, alternate: 'geonode:fgb_layer' });
});

it('arcgis layer includes alternate in extendedParams', () => {
const newLayer = resourceToLayerConfig({
alternate: 'remoteWorkspace:1',
title: 'Layer title',
perms: [],
links: [{
extension: 'html',
link_type: 'image',
mime: 'text/html',
name: 'ArcGIS REST ImageServer',
url: '/MapServer'
}],
pk: 5,
ptype: 'gxp_arcrestsource'
});
expect(newLayer.extendedParams).toEqual({ pk: 5, alternate: 'remoteWorkspace:1' });
});
});
});

describe('getDimensions', () => {
it('should return empty array if no links and has_time is false', () => {
const result = getDimensions();
expect(result).toEqual([]);
});

it('should return dimensions with time if has_time is true and WMTS link is present', () => {
const links = [{ link_type: 'OGC:WMTS', url: 'http://example.com/wmts' }];
const result = getDimensions({ links, has_time: true });
expect(result).toEqual([{
name: 'time',
source: {
type: 'multidim-extension',
url: 'http://example.com/wmts'
}
}]);
});

it('should return dimensions with time if has_time is true and only WMS link is present', () => {
const links = [{ link_type: 'OGC:WMS', url: 'http://example.com/geoserver/wms' }];
const result = getDimensions({ links, has_time: true });
expect(result).toEqual([{
name: 'time',
source: {
type: 'multidim-extension',
url: 'http://example.com/geoserver/gwc/service/wmts'
}
}]);
});

it('should return empty array if has_time is false', () => {
const links = [{ link_type: 'OGC:WMTS', url: 'http://example.com/wmts' }];
const result = getDimensions({ links, has_time: false });
expect(result).toEqual([]);
});

it('should return default url if no matching link types are found', () => {
const links = [{ link_type: 'OGC:OTHER', url: 'http://example.com/other' }];
const result = getDimensions({ links, has_time: true });
expect(result).toEqual([{
name: 'time',
source: {
type: 'multidim-extension',
url: '/geoserver/gwc/service/wmts'
}
}]);
});
});
});
10 changes: 10 additions & 0 deletions web/client/utils/__tests__/LocaleUtils-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,14 @@ describe('LocaleUtils', () => {
expect(Object.keys(LocaleUtils.DATE_FORMATS).length).toBe(10);
expect(Object.keys(LocaleUtils.DATE_FORMATS)).toEqual(["default", "en-US", "it-IT", "nl-NL", "zh-ZH", "hr-HR", "pt-PT", "pt-BR", "vi-VN", "fi-FI"]);
});
it('longLocale should return language-region format', () => {
expect(LocaleUtils.longLocale('en')).toMatch(/en-[A-Z]{2}/);
expect(LocaleUtils.longLocale('en-GB')).toBe('en-GB');
expect(LocaleUtils.longLocale('invalid code')).toBe('');
});
it('shortLocale should return the language component of a locale code', () => {
expect(LocaleUtils.shortLocale('en-US')).toBe('en');
expect(LocaleUtils.shortLocale('it-IT')).toBe('it');
expect(LocaleUtils.shortLocale('invalid code')).toBe('');
});
});
Loading