diff --git a/docs/developer-guide/maps-configuration.md b/docs/developer-guide/maps-configuration.md index 8c3afbe558..e869bdb82f 100644 --- a/docs/developer-guide/maps-configuration.md +++ b/docs/developer-guide/maps-configuration.md @@ -208,6 +208,7 @@ In the case of the background the `thumbURL` is used to show a preview of the la - `empty`: special type for empty background - `3dtiles`: 3d tiles layers - `terrain`: layers that define the elevation profile of the terrain +- `cog`: Cloud Optimized GeoTIFF layers #### WMS @@ -1493,6 +1494,24 @@ In order to use these layers they need to be added to the `additionalLayers` in } ``` +#### Cloud Optimized GeoTIFF (COG) + +i.e. + +```javascript +{ + "type": "cog", + "title": "Title", + "group": "background", + "visibility": false, + "name": "Name", + "sources": [ + { "url": "https://host-sample/cog1.tif" }, + { "url": "https://host-sample/cog2.tif" } + ] +} +``` + ## Layer groups Inside the map configuration, near the `layers` entry, you can find also the `groups` entry. This array contains information about the groups in the TOC. diff --git a/docs/user-guide/catalog.md b/docs/user-guide/catalog.md index ed45266c32..c4eacc4835 100644 --- a/docs/user-guide/catalog.md +++ b/docs/user-guide/catalog.md @@ -346,3 +346,8 @@ In **general settings of** 3D Tiles service, the user can specify the title to a Since the Google Photorealistic 3D Tiles are not ‘survey-grade’ at this time, the use of certain MapStore tools could be considered derivative and, for this reason, prohibited. Please, make sure you have read the [Google conditions of use](https://developers.google.com/maps/documentation/tile/policies) (some [FAQs](https://cloud.google.com/blog/products/maps-platform/commonly-asked-questions-about-our-recently-launched-photorealistic-3d-tiles) are also available online for this purpose) before providing Google Photorealistic 3D Tile in your MapStore maps in order to enable only allowed tools (e.g. *Measurement* and *Identify* tools should be probably disabled). For this purpose it is possible to appropriately set the [configuration of MapStore plugins](../../developer-guide/maps-configuration/#map-options) to exclude tools that could conflict with Google policies. Alternatively, it is possible to use a dedicated [application context](application-context.md#configure-plugins) to show Photorealistic 3D Tiles by including only the permitted tools within it. + +### Cloud Optimized GeoTIFF + +A Cloud Optimized GeoTIFF (COG) is a regular GeoTIFF file, aimed at being hosted on a HTTP file server, with an internal organization that enables more efficient workflows on the cloud. It does this by leveraging the ability of clients issuing ​HTTP GET range requests to ask for just the parts of a file they need. +MapStore allows to add COG as layers and backgrounds. Through the Catalog tool, a multiple url sources of COG are obtained and converted to layers as each url corresponds to a layer diff --git a/web/client/api/catalog/COG.js b/web/client/api/catalog/COG.js new file mode 100644 index 0000000000..56f325e508 --- /dev/null +++ b/web/client/api/catalog/COG.js @@ -0,0 +1,114 @@ +/* + * Copyright 2023, 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 get from 'lodash/get'; +import { Observable } from 'rxjs'; +import { isValidURL } from '../../utils/URLUtils'; + +export const COG_LAYER_TYPE = 'cog'; +const searchAndPaginate = (layers, startPosition, maxRecords, text) => { + + const filteredLayers = layers + .filter(({ title = "" } = {}) => !text + || title.toLowerCase().indexOf(text.toLowerCase()) !== -1 + ); + const records = filteredLayers + .filter((layer, index) => index >= startPosition - 1 && index < startPosition - 1 + maxRecords); + return { + numberOfRecordsMatched: filteredLayers.length, + numberOfRecordsReturned: records.length, + nextRecord: startPosition + Math.min(maxRecords, filteredLayers.length) + 1, + records + }; +}; +export const getRecords = (url, startPosition, maxRecords, text, info = {}) => { + const service = get(info, 'options.service'); + let layers = []; + if (service.url) { + const urls = service.url?.split(',')?.map(_url => _url?.trim()); + // each url corresponds to a layer + layers = urls.map((_url, index) => { + const title = _url.split('/')?.pop()?.replace('.tif', '') || `COG_${index}`; + return { + ...service, + title, + type: COG_LAYER_TYPE, + sources: [{url: _url}], + options: service.options || {} + }; + }); + } + // fake request with generated layers + return new Promise((resolve) => { + resolve(searchAndPaginate(layers, startPosition, maxRecords, text)); + }); + + +}; + +export const textSearch = (url, startPosition, maxRecords, text, info = {}) => { + return getRecords(url, startPosition, maxRecords, text, info); +}; + +const validateCog = (service) => { + const urls = service.url?.split(','); + const isValid = urls.every(url => isValidURL(url?.trim())); + if (service.title && isValid) { + return Observable.of(service); + } + const error = new Error("catalog.config.notValidURLTemplate"); + // insert valid URL; + throw error; +}; +export const validate = service => { + return validateCog(service); +}; +export const testService = service => { + return Observable.of(service); +}; + +export const getCatalogRecords = (data) => { + if (data && data.records) { + return data.records.map(record => { + return { + serviceType: COG_LAYER_TYPE, + isValid: record.sources?.every(source => isValidURL(source.url)), + title: record.title || record.provider, + sources: record.sources, + options: record.options, + references: [] + }; + }); + } + return null; +}; + +/** + * Converts a record into a layer + */ +export const cogToLayer = (record) => { + return { + type: COG_LAYER_TYPE, + visibility: true, + sources: record.sources, + title: record.title, + options: record.options, + name: record.title + }; +}; + +const recordToLayer = (record, options) => { + return cogToLayer(record, options); +}; + +export const getLayerFromRecord = (record, options, asPromise) => { + if (asPromise) { + return Promise.resolve(recordToLayer(record, options)); + } + return recordToLayer(record, options); +}; diff --git a/web/client/api/catalog/__tests__/COG-test.js b/web/client/api/catalog/__tests__/COG-test.js new file mode 100644 index 0000000000..c38645db7d --- /dev/null +++ b/web/client/api/catalog/__tests__/COG-test.js @@ -0,0 +1,64 @@ +/* + * Copyright 2023, 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 { getLayerFromRecord, getCatalogRecords, validate, COG_LAYER_TYPE} from '../COG'; +import expect from 'expect'; + + +const record = {sources: [{url: "some.tif"}], title: "some", options: []}; +describe('COG (Abstraction) API', () => { + beforeEach(done => { + setTimeout(done); + }); + + afterEach(done => { + setTimeout(done); + }); + it('test getLayerFromRecord', () => { + const layer = getLayerFromRecord(record, null); + expect(layer.title).toBe(record.title); + expect(layer.visibility).toBeTruthy(); + expect(layer.type).toBe(COG_LAYER_TYPE); + expect(layer.sources).toEqual(record.sources); + expect(layer.name).toBe(record.title); + }); + it('test getLayerFromRecord as promise', () => { + getLayerFromRecord(record, null, true).then((layer) => { + expect(layer.title).toBe(record.title); + expect(layer.visibility).toBeTruthy(); + expect(layer.type).toBe(COG_LAYER_TYPE); + expect(layer.sources).toEqual(record.sources); + expect(layer.name).toBe(record.title); + }); + }); + it('test getCatalogRecords - empty records', () => { + const catalogRecords = getCatalogRecords(); + expect(catalogRecords).toBeFalsy(); + }); + it('test getCatalogRecords', () => { + const records = getCatalogRecords({records: [record]}); + const [{serviceType, isValid, title, sources, options }] = records; + expect(serviceType).toBe(COG_LAYER_TYPE); + expect(isValid).toBeFalsy(); + expect(title).toBe(record.title); + expect(sources).toEqual(record.sources); + expect(options).toEqual(record.options); + }); + it('test validate with invalid url', () => { + const service = {title: "some", url: "some.tif"}; + const error = new Error("catalog.config.notValidURLTemplate"); + try { + validate(service); + } catch (e) { + expect(e).toEqual(error); + } + }); + it('test validate with valid url', () => { + const service = {title: "some", url: "https://some.tif"}; + expect(validate(service)).toBeTruthy(); + }); +}); diff --git a/web/client/api/catalog/index.js b/web/client/api/catalog/index.js index 0d460d68ed..3e72a62391 100644 --- a/web/client/api/catalog/index.js +++ b/web/client/api/catalog/index.js @@ -15,6 +15,7 @@ import * as wfs from './WFS'; import * as geojson from './GeoJSON'; import * as backgrounds from './backgrounds'; import * as threeDTiles from './ThreeDTiles'; +import * as cog from './COG'; /** * APIs collection for catalog. @@ -49,5 +50,6 @@ export default { 'wmts': wmts, 'geojson': geojson, 'backgrounds': backgrounds, - '3dtiles': threeDTiles + '3dtiles': threeDTiles, + 'cog': cog }; diff --git a/web/client/components/TOC/DefaultLayer.jsx b/web/client/components/TOC/DefaultLayer.jsx index c5a077fff6..3fbf4b3b64 100644 --- a/web/client/components/TOC/DefaultLayer.jsx +++ b/web/client/components/TOC/DefaultLayer.jsx @@ -97,7 +97,9 @@ class DefaultLayer extends React.Component { }; getVisibilityMessage = () => { - if (this.props.node.exclusiveMapType) return this.props.node?.type === '3dtiles' && 'toc.notVisibleSwitchTo3D'; + if (this.props.node.exclusiveMapType) { + return this.props.node?.type === '3dtiles' ? 'toc.notVisibleSwitchTo3D' : this.props.node?.type === 'cog' ? 'toc.notVisibleSwitchTo2D' : ''; + } const maxResolution = this.props.node.maxResolution || Infinity; return this.props.resolution >= maxResolution ? 'toc.notVisibleZoomIn' diff --git a/web/client/components/background/BackgroundSelector.jsx b/web/client/components/background/BackgroundSelector.jsx index 67fe77597f..ce6593e3c8 100644 --- a/web/client/components/background/BackgroundSelector.jsx +++ b/web/client/components/background/BackgroundSelector.jsx @@ -126,7 +126,7 @@ class BackgroundSelector extends React.Component { }} tooltipId="backgroundSelector.deleteTooltip" />} - {this.props.mapIsEditable && !this.props.enabledCatalog && !!(layer.type === 'wms' || layer.type === 'wmts' || layer.type === 'tms' || layer.type === 'tileprovider') && + {this.props.mapIsEditable && !this.props.enabledCatalog && ['wms', 'wmts', 'tms', 'tileprovider', 'cog'].includes(layer.type) && ); }; +const COGEditor = ({ service = {}, onChangeUrl = () => { } }) => { + return ( + + +   } /> + onChangeUrl(e.target.value)}/> + + + + ); +}; + /** * Main Form for editing a catalog entry @@ -137,7 +156,7 @@ export default ({ useEffect(() => { !isEmpty(service.url) && handleProtocolValidity(service.url); }, [service?.allowUnsecureLayers]); - const URLEditor = service.type === "tms" ? TmsURLEditor : DefaultURLEditor; + const URLEditor = service.type === "tms" ? TmsURLEditor : service.type === "cog" ? COGEditor : DefaultURLEditor; return (
diff --git a/web/client/components/catalog/editor/MainFormUtils.js b/web/client/components/catalog/editor/MainFormUtils.js index e993e1c25f..389f7b0dd1 100644 --- a/web/client/components/catalog/editor/MainFormUtils.js +++ b/web/client/components/catalog/editor/MainFormUtils.js @@ -7,7 +7,8 @@ export const defaultPlaceholder = (service) => { "wms": "e.g. https://mydomain.com/geoserver/wms", "csw": "e.g. https://mydomain.com/geoserver/csw", "tms": "e.g. https://mydomain.com/geoserver/gwc/service/tms/1.0.0", - "3dtiles": "e.g. https://mydomain.com/tileset.json" + "3dtiles": "e.g. https://mydomain.com/tileset.json", + "cog": "e.g. https://mydomain.com/cog.tif" }; for ( const [key, value] of Object.entries(urlPlaceholder)) { if ( key === service.type) { diff --git a/web/client/components/map/openlayers/plugins/COGLayer.js b/web/client/components/map/openlayers/plugins/COGLayer.js new file mode 100644 index 0000000000..5941f94d64 --- /dev/null +++ b/web/client/components/map/openlayers/plugins/COGLayer.js @@ -0,0 +1,45 @@ +/** + * Copyright 2015, 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 Layers from '../../../../utils/openlayers/Layers'; + +import GeoTIFF from 'ol/source/GeoTIFF.js'; +import TileLayer from 'ol/layer/WebGLTile.js'; + +function create(options) { + return new TileLayer({ + msId: options.id, + style: options.style, // TODO style needs to be improved. Currently renders only predefined band and ranges when specified in config + opacity: options.opacity !== undefined ? options.opacity : 1, + visible: options.visibility, + source: new GeoTIFF({ + convertToRGB: 'auto', // CMYK, YCbCr, CIELab, and ICCLab images will automatically be converted to RGB + sources: options.sources, + wrapX: true + }), + zIndex: options.zIndex, + minResolution: options.minResolution, + maxResolution: options.maxResolution + }); +} + +Layers.registerType('cog', { + create, + update(layer, newOptions, oldOptions, map) { + if (newOptions.srs !== oldOptions.srs) { + return create(newOptions, map); + } + if (oldOptions.minResolution !== newOptions.minResolution) { + layer.setMinResolution(newOptions.minResolution === undefined ? 0 : newOptions.minResolution); + } + if (oldOptions.maxResolution !== newOptions.maxResolution) { + layer.setMaxResolution(newOptions.maxResolution === undefined ? Infinity : newOptions.maxResolution); + } + return null; + } +}); diff --git a/web/client/components/map/openlayers/plugins/index.js b/web/client/components/map/openlayers/plugins/index.js index a232b2258d..848bc73219 100644 --- a/web/client/components/map/openlayers/plugins/index.js +++ b/web/client/components/map/openlayers/plugins/index.js @@ -19,5 +19,6 @@ export default { WFSLayer: require('./WFSLayer').default, WFS3Layer: require('./WFS3Layer').default, WMSLayer: require('./WMSLayer').default, - WMTSLayer: require('./WMTSLayer').default + WMTSLayer: require('./WMTSLayer').default, + COGLayer: require('./COGLayer').default }; diff --git a/web/client/plugins/MetadataExplorer.jsx b/web/client/plugins/MetadataExplorer.jsx index 3e672df062..e832b9eab2 100644 --- a/web/client/plugins/MetadataExplorer.jsx +++ b/web/client/plugins/MetadataExplorer.jsx @@ -177,7 +177,7 @@ class MetadataExplorerComponent extends React.Component { static defaultProps = { id: "mapstore-metadata-explorer", - serviceTypes: [{ name: "csw", label: "CSW" }, { name: "wms", label: "WMS" }, { name: "wmts", label: "WMTS" }, { name: "tms", label: "TMS", allowedProviders: DEFAULT_ALLOWED_PROVIDERS }, { name: "wfs", label: "WFS" }, { name: "3dtiles", label: "3D Tiles" }], + serviceTypes: [{ name: "csw", label: "CSW" }, { name: "wms", label: "WMS" }, { name: "wmts", label: "WMTS" }, { name: "tms", label: "TMS", allowedProviders: DEFAULT_ALLOWED_PROVIDERS }, { name: "wfs", label: "WFS" }, { name: "3dtiles", label: "3D Tiles" }, { name: "cog", label: "COG" }], active: false, wrap: false, modal: true, @@ -277,7 +277,7 @@ const MetadataExplorerPlugin = connect(metadataExplorerSelector, { })(MetadataExplorerComponent); /** - * MetadataExplorer (Catalog) plugin. Shows the catalogs results (CSW, WMS, WMTS, TMS and WFS). + * MetadataExplorer (Catalog) plugin. Shows the catalogs results (CSW, WMS, WMTS, TMS, WFS and COG). * Some useful flags in `localConfig.json`: * - `noCreditsFromCatalog`: avoid add credits (attribution) from catalog * @@ -285,7 +285,7 @@ const MetadataExplorerPlugin = connect(metadataExplorerSelector, { * @name MetadataExplorer * @memberof plugins * @prop {string} cfg.hideThumbnail shows/hides thumbnail - * @prop {object[]} cfg.serviceTypes Service types available to add a new catalog. default: `[{ name: "csw", label: "CSW" }, { name: "wms", label: "WMS" }, { name: "wmts", label: "WMTS" }, { name: "tms", label: "TMS", allowedProviders },{ name: "wfs", label: "WFS" }]`. + * @prop {object[]} cfg.serviceTypes Service types available to add a new catalog. default: `[{ name: "csw", label: "CSW" }, { name: "wms", label: "WMS" }, { name: "wmts", label: "WMTS" }, { name: "tms", label: "TMS", allowedProviders },{ name: "wfs", label: "WFS" }, { name: "cog", label: "COG" }]`. * `allowedProviders` is a whitelist of tileProviders from ConfigProvider.js. you can set a global variable allowedProviders in localConfig.json to set it up globally. You can configure it to "ALL" to get all the list (at your own risk, some services could change or not be available anymore) * @prop {object} cfg.hideIdentifier shows/hides identifier * @prop {boolean} cfg.hideExpand shows/hides full description button diff --git a/web/client/plugins/Print.jsx b/web/client/plugins/Print.jsx index 7c581d2bec..937c75c5c4 100644 --- a/web/client/plugins/Print.jsx +++ b/web/client/plugins/Print.jsx @@ -225,6 +225,11 @@ function mergeItems(standard, overrides) { .map(handleRemoved); } +function filterLayer(layer = {}) { + // Skip layer with error and type cog + return !layer.loadingError && layer.type !== "cog"; +} + export default { PrintPlugin: assign({ loadPlugin: (resolve) => { @@ -633,7 +638,7 @@ export default { error, map, layers: [ - ...layers.filter(l => !l.loadingError), + ...layers.filter(filterLayer), ...(printSpec?.additionalLayers ? additionalLayers.map(l => l.options).filter( l => { const isVector = l.type === 'vector'; diff --git a/web/client/plugins/TOC.jsx b/web/client/plugins/TOC.jsx index f119ca562a..f8ce3cdab4 100644 --- a/web/client/plugins/TOC.jsx +++ b/web/client/plugins/TOC.jsx @@ -154,7 +154,7 @@ const tocSelector = createShallowSelectorCreator(isEqual)( }, { options: { exclusiveMapType: true }, - func: (node) => node.type === "3dtiles" && !isCesiumActive + func: (node) => (node.type === "3dtiles" && !isCesiumActive) || (node.type === "cog" && isCesiumActive) } ]), catalogActive, diff --git a/web/client/translations/data.de-DE.json b/web/client/translations/data.de-DE.json index f55e969a03..3d35ec1fb6 100644 --- a/web/client/translations/data.de-DE.json +++ b/web/client/translations/data.de-DE.json @@ -630,6 +630,7 @@ "notVisibleZoomIn": "Die Ebene ist nicht sichtbar, da sie außerhalb der Auflösungsgrenzen liegt. Zoomen Sie hinein, um die Ebene anzuzeigen", "notVisibleZoomOut": "Die Ebene ist nicht sichtbar, da sie außerhalb der Auflösungsgrenzen liegt. Verkleinern Sie die Ansicht, um die Ebene anzuzeigen", "notVisibleSwitchTo3D": "Wechseln Sie in den 3D-Kartenmodus, um diesen Layer anzuzeigen", + "notVisibleSwitchTo2D": "Wechseln Sie in den 2D-Kartenmodus, um diesen Layer anzuzeigen", "refreshOptions": { "bbox": "BBOX aktualisieren", "search": "Suchoptionen aktualisieren", @@ -1569,6 +1570,10 @@ "tileprovider": { "tooltip": "x, y, z sind Kachelpositionen, s ist die Unterdomäne (Standard)" }, + "urls": "URL(s)", + "cog": { + "urlTemplateHint": "Mehrere URLs können durch Komma-Trennung hinzugefügt werden. Jede URL wird als einzelne Ebene behandelt" + }, "missingReference": "Fehlende OGC-Referenzmetadaten", "showDescription": "Vollständige Beschreibung anzeigen", "hideDescription": "Vollständige Beschreibung ausblenden", diff --git a/web/client/translations/data.en-US.json b/web/client/translations/data.en-US.json index 544bfb6750..c78ac3b2e7 100644 --- a/web/client/translations/data.en-US.json +++ b/web/client/translations/data.en-US.json @@ -591,6 +591,7 @@ "notVisibleZoomIn": "The layer is not visible because it is outside the resolution limits. Zoom in to show the layer", "notVisibleZoomOut": "The layer is not visible because it is outside the resolution limits. Zoom out to show the layer", "notVisibleSwitchTo3D": "Switch to 3D map mode in order to see this layer", + "notVisibleSwitchTo2D": "Switch to 2D map mode in order to see this layer", "refreshOptions": { "bbox": "Update BBOX", "search": "Update search options", @@ -1528,6 +1529,10 @@ "forceDefaultTileGrid": "Force default tile grid", "forceDefaultTileGridDescription": "Use the global projection's tile grid instead of the origin and resolutions provided by the server. This is useful for some TMS services that advertise wrong origin or resolutions." }, + "urls": "URL(s)", + "cog": { + "urlTemplateHint": "Multiple urls can be added by comma separation. Each url will be treated as an individual layer" + }, "tileprovider": { "tooltip": "x,y,z are tile position, s is the sub-domain (default)" }, diff --git a/web/client/translations/data.es-ES.json b/web/client/translations/data.es-ES.json index 901eb0620e..a9a4c76c27 100644 --- a/web/client/translations/data.es-ES.json +++ b/web/client/translations/data.es-ES.json @@ -591,6 +591,7 @@ "notVisibleZoomIn": "La capa no es visible porque está fuera de los límites de resolución. Acercar para mostrar la capa", "notVisibleZoomOut": "La capa no es visible porque está fuera de los límites de resolución. Alejar para mostrar la capa", "notVisibleSwitchTo3D": "Cambie al modo de mapa 3D para ver esta capa", + "notVisibleSwitchTo2D": "Cambie al modo de mapa 2D para ver esta capa", "refreshOptions": { "bbox": "Refrescar BBOX", "search": "Refrescar las opciones de búsqueda", @@ -1532,6 +1533,10 @@ "tileprovider": { "tooltip": "x, y, z son la posición del mosaico, s es el subdominio (predeterminado)" }, + "urls": "URL(s)", + "cog": { + "urlTemplateHint": "Se pueden agregar varias URL mediante separación por comas. Cada URL será tratada como una capa individual." + }, "missingReference": "Faltan metadatos de referencia OGC", "showDescription": "Mostrar descripción completa", "hideDescription": "Ocultar descripción completa", diff --git a/web/client/translations/data.fr-FR.json b/web/client/translations/data.fr-FR.json index a84f51f223..52cdfebf3a 100644 --- a/web/client/translations/data.fr-FR.json +++ b/web/client/translations/data.fr-FR.json @@ -591,6 +591,7 @@ "notVisibleZoomIn": "Le calque n'est pas visible car il est en dehors des limites de résolution. Zoomez pour afficher le calque", "notVisibleZoomOut": "Le calque n'est pas visible car il est en dehors des limites de résolution. Faites un zoom arrière pour afficher le calque", "notVisibleSwitchTo3D": "Passer en mode carte 3D pour voir cette couche", + "notVisibleSwitchTo2D": "Passer en mode carte 2D pour voir cette couche", "refreshOptions": { "bbox": "Mettre à jour la BBOX", "search": "Mettre à jour les options de recherche", @@ -1532,6 +1533,10 @@ "tileprovider": { "tooltip": "x, y, z sont la position des tuiles, s est le sous-domaine (par défaut)" }, + "urls": "URL(s)", + "cog": { + "urlTemplateHint": "Plusieurs URL peuvent être ajoutées par séparation par des virgules. Chaque URL sera traitée comme une couche individuelle" + }, "missingReference": "Les référence OGC des métadonnées sont manquantes", "showDescription": "Afficher la description complète", "hideDescription": "Masquer la description complète", diff --git a/web/client/translations/data.it-IT.json b/web/client/translations/data.it-IT.json index 6194fc2478..b3c4c450e5 100644 --- a/web/client/translations/data.it-IT.json +++ b/web/client/translations/data.it-IT.json @@ -591,6 +591,7 @@ "notVisibleZoomIn": "Il layer non è visibile perchè al di fuori dei limiti di risoluzione. Zoom in per mostrare il layer", "notVisibleZoomOut": "Il layer non è visibile perchè al di fuori dei limiti di risoluzione. Zoom out per mostrare il layer", "notVisibleSwitchTo3D": "Passa alla modalità mappa 3D per vedere questo layer", + "notVisibleSwitchTo2D": "Passa alla modalità mappa 2D per vedere questo layer", "refreshOptions": { "bbox": "Aggiorna area di zoom", "search": "Aggiorna opzioni di ricerca", @@ -1531,6 +1532,10 @@ "tileprovider": { "tooltip": "x, y, z sono la posizione della tile, s è il sottodominio (impostazione predefinita)" }, + "urls": "URL(s)", + "cog": { + "urlTemplateHint": "È possibile aggiungere più URL separandoli con virgole. Ogni URL verrà trattato come un livello individuale" + }, "missingReference": "Metadati di riferimento OGC mancanti", "showDescription": "Mostra descrizione completa", "hideDescription": "Nascondi descrizione completa", diff --git a/web/client/utils/LayersUtils.js b/web/client/utils/LayersUtils.js index 8270cae194..02a4342316 100644 --- a/web/client/utils/LayersUtils.js +++ b/web/client/utils/LayersUtils.js @@ -643,6 +643,7 @@ export const saveLayer = (layer) => { tileSize: layer.tileSize, version: layer.version }, + layer.sources ? { sources: layer.sources } : {}, layer.heightOffset ? { heightOffset: layer.heightOffset } : {}, layer.params ? { params: layer.params } : {}, layer.extendedParams ? { extendedParams: layer.extendedParams } : {}, diff --git a/web/client/utils/__tests__/MapUtils-test.js b/web/client/utils/__tests__/MapUtils-test.js index 0c6619477a..2b632bd0c5 100644 --- a/web/client/utils/__tests__/MapUtils-test.js +++ b/web/client/utils/__tests__/MapUtils-test.js @@ -304,6 +304,7 @@ describe('Test the MapUtils', () => { visibility: true, catalogURL: "url", origin: [100000, 100000], + sources: [{url: "url"}], extendedParams: { fromExtension1: { testBool: true @@ -418,6 +419,7 @@ describe('Test the MapUtils', () => { visibility: true, catalogURL: "url", origin: [100000, 100000], + sources: [{url: "url"}], extendedParams: { fromExtension1: { testBool: true