diff --git a/web/client/api/catalog/COG.js b/web/client/api/catalog/COG.js index a566aa4813..9ba5a8c611 100644 --- a/web/client/api/catalog/COG.js +++ b/web/client/api/catalog/COG.js @@ -86,6 +86,9 @@ export const getRecords = (_url, startPosition, maxRecords, text, info = {}) => const isProjectionDefined = isProjectionAvailable(crs); layer = { ...layer, + sourceMetadata: { + crs + }, // skip adding bbox when geokeys or extent is empty ...(!isEmpty(extent) && !isEmpty(crs) && isProjectionDefined && { bbox: { diff --git a/web/client/components/TOC/DefaultLayer.jsx b/web/client/components/TOC/DefaultLayer.jsx index 254d2fe511..2fb8f61ada 100644 --- a/web/client/components/TOC/DefaultLayer.jsx +++ b/web/client/components/TOC/DefaultLayer.jsx @@ -10,7 +10,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import Node from './Node'; -import { isObject, castArray, find } from 'lodash'; +import { isObject, castArray, find, isNil } from 'lodash'; import { Grid, Row, Col, Glyphicon } from 'react-bootstrap'; import draggableComponent from './enhancers/draggableComponent'; import VisibilityCheck from './fragments/VisibilityCheck'; @@ -23,6 +23,7 @@ import tooltip from '../misc/enhancers/tooltip'; import localizedProps from '../misc/enhancers/localizedProps'; import { isInsideResolutionsLimits, getLayerTypeGlyph } from '../../utils/LayersUtils'; import StyleBasedLegend from './fragments/StyleBasedLegend'; +import { isSRSAllowed } from '../../utils/CoordinatesUtils'; const GlyphIndicator = localizedProps('tooltip')(tooltip(Glyphicon)); @@ -106,6 +107,17 @@ class DefaultLayer extends React.Component { : 'toc.notVisibleZoomOut'; }; + getErrorTooltipParams = () => { + if (!this.isCRSCompatible()) { + return { + tooltip: "toc.sourceCRSNotCompatible", + msgParams: {sourceCRS: this.getSourceCRS()} + }; + } + return { tooltip: "toc.loadingerror" }; + } + getSourceCRS = () => this.props.node?.bbox?.crs || this.props.node?.sourceMetadata?.crs; + renderOpacitySlider = (hideOpacityTooltip) => { return (this.props.activateOpacityTool && this.props.node?.type !== '3dtiles') ? ( { - return this.props.node.loadingError === 'Error' ? + return this.isLayerError() ? () : ( { - return this.props.node.loadingError === 'Error' || isEmpty ? + return this.isLayerError() || isEmpty ? null : ( {other.isDraggable && !isDummy ? this.props.connectDragPreview(head) : head} - {isDummy || !this.props.activateOpacityTool || this.props.node.expanded || !this.props.node.visibility || this.props.node.loadingError === 'Error' ? null : this.renderOpacitySlider(this.props.hideOpacityTooltip)} + {isDummy || !this.props.activateOpacityTool || this.props.node.expanded || !this.props.node.visibility || this.isLayerError() ? null : this.renderOpacitySlider(this.props.hideOpacityTooltip)} {isDummy || isEmpty ? null : this.renderCollapsible()} ); @@ -212,7 +224,7 @@ class DefaultLayer extends React.Component { let {children, propertiesChangeHandler, onToggle, connectDragSource, connectDropTarget, ...other } = this.props; const hide = !this.props.node.visibility || this.props.node.invalid || this.props.node.exclusiveMapType || !isInsideResolutionsLimits(this.props.node, this.props.resolution) ? ' visibility' : ''; const selected = this.props.selectedNodes.filter((s) => s === this.props.node.id).length > 0 ? ' selected' : ''; - const error = this.props.node.loadingError === 'Error' ? ' layer-error' : ''; + const error = this.isLayerError() ? ' layer-error' : ''; const warning = this.props.node.loadingError === 'Warning' ? ' layer-warning' : ''; const grab = other.isDraggable ? : ; const isDummy = !!this.props.node.dummy; @@ -234,6 +246,13 @@ class DefaultLayer extends React.Component { return (title || '').toLowerCase().indexOf(this.props.filterText.toLowerCase()) !== -1; }; + isLayerError = () => this.props.node.loadingError === 'Error' || !this.isCRSCompatible(); + + isCRSCompatible = () => { + const CRS = this.getSourceCRS(); + // Check if source crs is compatible + return !isNil(CRS) ? isSRSAllowed(CRS) : true; + } } export default draggableComponent('LayerOrGroup', DefaultLayer); diff --git a/web/client/components/TOC/__tests__/DefaultLayer-test.jsx b/web/client/components/TOC/__tests__/DefaultLayer-test.jsx index f68aace8dd..2c9ca943d1 100644 --- a/web/client/components/TOC/__tests__/DefaultLayer-test.jsx +++ b/web/client/components/TOC/__tests__/DefaultLayer-test.jsx @@ -466,4 +466,43 @@ describe('test DefaultLayer module component', () => { expect(button.length).toBe(1); } }); + it('test with layer source crs', () => { + // Invalid CRS + let node = { + name: 'layer00', + title: 'Layer', + visibility: false, + storeIndex: 9, + opacity: 0.5, + bbox: { + crs: "EPSG:3946" + } + }; + + let comp = ReactDOM.render(, document.getElementById("container")); + expect(ReactDOM.findDOMNode(comp)).toBeTruthy(); + let layerNode = document.querySelector('.toc-default-layer.layer-error'); + let errorTooltip = document.querySelector('.toc-layer-tool.toc-error'); + expect(layerNode).toBeTruthy(); + expect(errorTooltip).toBeTruthy(); + + // Valid CRS + node = { + name: 'layer00', + title: 'Layer', + visibility: false, + storeIndex: 9, + opacity: 0.5, + bbox: { + crs: "EPSG:4326" + } + }; + + comp = ReactDOM.render(, document.getElementById("container")); + expect(ReactDOM.findDOMNode(comp)).toBeTruthy(); + layerNode = document.querySelector('.toc-default-layer.layer-error'); + errorTooltip = document.querySelector('.toc-layer-tool.toc-error'); + expect(layerNode).toBeFalsy(); + expect(errorTooltip).toBeFalsy(); + }); }); diff --git a/web/client/components/TOC/fragments/LayersTool.jsx b/web/client/components/TOC/fragments/LayersTool.jsx index 2f81933b45..b2b8116d68 100644 --- a/web/client/components/TOC/fragments/LayersTool.jsx +++ b/web/client/components/TOC/fragments/LayersTool.jsx @@ -21,6 +21,7 @@ class LayersTool extends React.Component { style: PropTypes.object, glyph: PropTypes.string, tooltip: PropTypes.string, + msgParams: PropTypes.object, className: PropTypes.string }; @@ -34,7 +35,7 @@ class LayersTool extends React.Component { glyph={this.props.glyph} onClick={() => this.props.onClick(this.props.node)}/>); return this.props.tooltip ? - )}> + )}> {tool} : tool; diff --git a/web/client/components/catalog/RecordItem.jsx b/web/client/components/catalog/RecordItem.jsx index 7fa87dc45b..b290d35047 100644 --- a/web/client/components/catalog/RecordItem.jsx +++ b/web/client/components/catalog/RecordItem.jsx @@ -14,7 +14,7 @@ import { buildSRSMap, getRecordLinks } from '../../utils/CatalogUtils'; -import {isAllowedSRS} from '../../utils/CoordinatesUtils'; +import { isAllowedSRS, isSRSAllowed } from '../../utils/CoordinatesUtils'; import HtmlRenderer from '../misc/HtmlRenderer'; import {parseCustomTemplate} from '../../utils/TemplateUtils'; import {getMessageById} from '../../utils/LocaleUtils'; @@ -125,6 +125,16 @@ class RecordItem extends React.Component { }; + isSRSNotAllowed = (record) => { + if (record.serviceType !== 'cog') { + const ogcReferences = record.ogcReferences || { SRS: [] }; + const allowedSRS = ogcReferences?.SRS?.length > 0 && buildSRSMap(ogcReferences.SRS); + return allowedSRS && !isAllowedSRS(this.props.crs, allowedSRS); + } + const crs = record?.sourceMetadata?.crs; + return crs && !isSRSAllowed(crs); + } + getButtons = (record) => { const links = this.props.showGetCapLinks ? getRecordLinks(record) : []; return [ @@ -136,9 +146,7 @@ class RecordItem extends React.Component { loading: this.state.loading, glyph: 'plus', onClick: () => { - const ogcReferences = record.ogcReferences || { SRS: [] }; - const allowedSRS = ogcReferences?.SRS?.length > 0 && buildSRSMap(ogcReferences.SRS); - if (allowedSRS && !isAllowedSRS(this.props.crs, allowedSRS)) { + if (this.isSRSNotAllowed(record)) { return this.props.onError('catalog.srs_not_allowed'); } this.setState({ loading: true }); diff --git a/web/client/components/catalog/__tests__/RecordItem-test.jsx b/web/client/components/catalog/__tests__/RecordItem-test.jsx index d4933ce90b..16b8bbd2c0 100644 --- a/web/client/components/catalog/__tests__/RecordItem-test.jsx +++ b/web/client/components/catalog/__tests__/RecordItem-test.jsx @@ -605,7 +605,7 @@ describe('This test for RecordItem', () => { } }; let actionsSpy = expect.spyOn(actions, "onError"); - const item = ReactDOM.render(, document.getElementById("container")); @@ -617,6 +617,22 @@ describe('This test for RecordItem', () => { expect(button).toBeTruthy(); button.click(); expect(actionsSpy.calls.length).toBe(1); + + // With source metadata + const record = {...sampleRecord2, serviceType: "cog", sourceMetadata: {crs: "EPSG:3946"}}; + item = ReactDOM.render(, document.getElementById("container")); + expect(item).toBeTruthy(); + + button = TestUtils.findRenderedDOMComponentWithTag( + item, 'button' + ); + expect(button).toBeTruthy(); + button.click(); + expect(actionsSpy.calls.length).toBe(2); + }); it('check add layer with bounding box', (done) => { let actions = { diff --git a/web/client/components/map/openlayers/plugins/COGLayer.js b/web/client/components/map/openlayers/plugins/COGLayer.js index 5941f94d64..591361b86b 100644 --- a/web/client/components/map/openlayers/plugins/COGLayer.js +++ b/web/client/components/map/openlayers/plugins/COGLayer.js @@ -10,6 +10,7 @@ import Layers from '../../../../utils/openlayers/Layers'; import GeoTIFF from 'ol/source/GeoTIFF.js'; import TileLayer from 'ol/layer/WebGLTile.js'; +import { isProjectionAvailable } from '../../../../utils/ProjectionUtils'; function create(options) { return new TileLayer({ @@ -41,5 +42,8 @@ Layers.registerType('cog', { layer.setMaxResolution(newOptions.maxResolution === undefined ? Infinity : newOptions.maxResolution); } return null; + }, + isCompatible: (layer) => { + return isProjectionAvailable(layer?.sourceMetadata?.crs); } }); diff --git a/web/client/translations/data.de-DE.json b/web/client/translations/data.de-DE.json index 826ec8c789..0fc6fd2273 100644 --- a/web/client/translations/data.de-DE.json +++ b/web/client/translations/data.de-DE.json @@ -626,6 +626,7 @@ "browseData": "Attributtabelle der ausgewählten Ebene öffnen", "removeLayer": "Ebene entfernen", "loadingerror": "Diese Ebene wurde nicht korrekt geladen oder ist nicht verfügbar", + "sourceCRSNotCompatible": "Die Quelldatenprojektionsdefinition {sourceCRS} des Layers ist in der Anwendung nicht für die Neuprojektion der Daten in der aktuellen Karte verfügbar", "measure": "Messen", "backgroundSwitcher": "Hintergrund", "layers": "Ebenen", diff --git a/web/client/translations/data.en-US.json b/web/client/translations/data.en-US.json index 0f422167aa..b41690ff12 100644 --- a/web/client/translations/data.en-US.json +++ b/web/client/translations/data.en-US.json @@ -588,6 +588,7 @@ "browseData": "Open Attribute Table", "removeLayer": "Remove layer", "loadingerror": "The layer has not been loaded correctly or not available.", + "sourceCRSNotCompatible": "The layer's source data projection definition {sourceCRS} is not available in the application for reprojecting the data in the current map", "measure": "Measure", "layers": "Layers", "drawerButton": "Layers", diff --git a/web/client/translations/data.es-ES.json b/web/client/translations/data.es-ES.json index efbb9bce36..13095dd2e0 100644 --- a/web/client/translations/data.es-ES.json +++ b/web/client/translations/data.es-ES.json @@ -588,6 +588,7 @@ "browseData": "Abrir la tabla de atributos", "removeLayer": "Borrar la capa", "loadingerror": "La capa no se ha cargado correctamente o no está disponible", + "sourceCRSNotCompatible": "La definición de proyección de datos de origen de la capa {sourceCRS} no está disponible en la aplicación para reproyectar los datos en el mapa actual", "measure": "Medir", "layers": "Capas", "drawerButton": "Capas", diff --git a/web/client/translations/data.fr-FR.json b/web/client/translations/data.fr-FR.json index 45e502647f..9451a8863f 100644 --- a/web/client/translations/data.fr-FR.json +++ b/web/client/translations/data.fr-FR.json @@ -588,6 +588,7 @@ "browseData": "Ouvrir la table d'attribut", "removeLayer": "Supprimer la couche", "loadingerror": "La couche n'a pas été chargée correctement ou n'est pas disponible.", + "sourceCRSNotCompatible": "La définition de projection des données sources de la couche {sourceCRS} n'est pas disponible dans l'application de reprojection des données dans la carte actuelle", "measure": "Mesurer", "layers": "Couches", "drawerButton": "Couches", diff --git a/web/client/translations/data.it-IT.json b/web/client/translations/data.it-IT.json index 675f13b097..d536815d4f 100644 --- a/web/client/translations/data.it-IT.json +++ b/web/client/translations/data.it-IT.json @@ -588,6 +588,7 @@ "browseData": "Apri tabella degli attributi", "removeLayer": "Rimuovi livello", "loadingerror": "Il livello non è stato caricato correttamente o non disponible.", + "sourceCRSNotCompatible": "La definizione di proiezione dei dati di origine del layer {sourceCRS} non è disponibile nell'applicazione per riproiettare i dati nella mappa corrente", "measure": "Misure", "layers": "Livelli", "drawerButton": "Livelli",