Skip to content

Commit 933eba4

Browse files
Fix geosolutions-it#11521 Support of COG layers in 3D viewer (geosolutions-it#11993)
1 parent db30db0 commit 933eba4

7 files changed

Lines changed: 144 additions & 14 deletions

File tree

package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@
104104
"webpack-dev-server": "3.11.0"
105105
},
106106
"dependencies": {
107+
"@babel/standalone": "7.23.9",
107108
"@carnesen/redux-add-action-listener-enhancer": "0.0.1",
108109
"@geosolutions/geostyler-geocss-parser": "1.0.0",
109110
"@geosolutions/geostyler-sld-parser": "2.0.1-2",
@@ -134,7 +135,6 @@
134135
"ajv": "8.17.1",
135136
"assert": "2.0.0",
136137
"axios": "0.30.2",
137-
"@babel/standalone": "7.23.9",
138138
"bootstrap": "3.4.1",
139139
"buffer": "6.0.3",
140140
"canvas-to-blob": "0.0.0",
@@ -161,6 +161,7 @@
161161
"flatgeobuf": "4.4.0",
162162
"font-awesome": "4.7.0",
163163
"fs-extra": "3.0.1",
164+
"geotiff": "2.1.3",
164165
"git-revision-webpack-plugin": "5.0.0",
165166
"history": "4.6.1",
166167
"html-to-draftjs": "npm:@geosolutions/html-to-draftjs@1.5.1",
@@ -248,6 +249,7 @@
248249
"shpjs": "3.4.2",
249250
"simple-statistics": "7.8.3",
250251
"stickybits": "3.6.6",
252+
"tiff-imagery-provider": "2.17.2",
251253
"tinycolor2": "1.4.1",
252254
"turf-bbox": "3.0.10",
253255
"turf-point": "2.0.1",
@@ -260,8 +262,7 @@
260262
"webfontloader": "1.6.28",
261263
"wellknown": "0.5.0",
262264
"xml2js": "0.6.2",
263-
"xpath": "0.0.27",
264-
"geotiff": "2.1.3"
265+
"xpath": "0.0.27"
265266
},
266267
"scripts": {
267268
"start": "npm run app:start",

web/client/api/catalog/COG.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ export const getRecords = (_url, startPosition, maxRecords, text, info = {}) =>
3737
const controller = get(info, 'options.controller');
3838
let layers = [];
3939
if (service.records) {
40-
// each record/url corresponds to a layer
4140
layers = service.records?.map((record) => {
4241
const url = record.url;
4342
let layer = {
@@ -69,9 +68,10 @@ export const getRecords = (_url, startPosition, maxRecords, text, info = {}) =>
6968
}
7069
return Promise.all([...layers]).then((_layers) => {
7170
if (!_layers.length) {
71+
const filename = _url.split('/').pop().split('.')[0];
7272
let layer = {
7373
...service,
74-
title: text,
74+
title: text || filename,
7575
identifier: _url,
7676
type: COG_LAYER_TYPE,
7777
sources: [{url: _url}],
@@ -101,7 +101,6 @@ const validateCog = (service) => {
101101
return Observable.of(service);
102102
}
103103
const error = new Error("catalog.config.notValidURLTemplate");
104-
// insert valid URL;
105104
throw error;
106105
};
107106
export const validate = service => {

web/client/components/map/cesium/Layer.jsx

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import React from 'react';
99

1010
import Layers from '../../../utils/cesium/Layers';
1111
import PropTypes from 'prop-types';
12-
import { round, isNil, castArray } from 'lodash';
12+
import { round, isNil, castArray, isFunction } from 'lodash';
1313
import { getResolutions } from '../../../utils/MapUtils';
1414
import axios from '../../../libs/ajax';
1515
import { getProxyCacheByUrl } from '../../../utils/ProxyUtils';
@@ -31,6 +31,7 @@ class CesiumLayer extends React.Component {
3131
// initial visibility should also take into account the visibility limits
3232
// in particular for detached layers (eg. Vector, WFS, 3D Tiles, ...)
3333
const visibility = this.getVisibilityOption(this.props);
34+
this._isMounted = true;
3435
this.createLayer(this.props.type, { ...this.props.options, visibility }, this.props.position, this.props.map, this.props.securityToken);
3536
if (this.props.options && this.layer && visibility) {
3637
this.addLayer(this.props);
@@ -76,6 +77,9 @@ class CesiumLayer extends React.Component {
7677
}
7778

7879
componentWillUnmount() {
80+
81+
this._isMounted = false;
82+
7983
if (this.layer && this.props.map && !this.props.map.isDestroyed()) {
8084
// detached layers are layers that do not work through a provider
8185
// for this reason they cannot be added or removed from the map imageryProviders
@@ -233,6 +237,22 @@ class CesiumLayer extends React.Component {
233237
...(this._isProxy ? { forceProxy: this._isProxy } : null)
234238
};
235239
this.layer = Layers.createLayer(type, opts, map);
240+
const isPromise = this.layer && isFunction(this.layer.then);
241+
if (isPromise) {
242+
this.layer.then((resolvedLayer) => {
243+
this.layer = resolvedLayer;
244+
this.provider = map.imageryLayers.addImageryProvider(resolvedLayer);
245+
if (this.layer) {
246+
this.layer.layerName = options.name;
247+
this.layer.layerId = options.id;
248+
}
249+
if (this.layer === null) {
250+
this.props.onCreationError(options);
251+
}
252+
this.props.map.scene.requestRender();
253+
});
254+
return;
255+
}
236256
if (this.layer) {
237257
this.layer.layerName = options.name;
238258
this.layer.layerId = options.id;
@@ -264,11 +284,31 @@ class CesiumLayer extends React.Component {
264284
},
265285
this.props.map);
266286
if (newLayer) {
287+
const isPromise = this.layer && isFunction(this.layer.then);
288+
if (isPromise) {
289+
this.layer.then((resolvedLayer) => {
290+
if (this._isMounted) {
291+
this.removeLayer();
292+
this.layer = resolvedLayer;
293+
294+
this.provider = this.props.map.imageryLayers.addImageryProvider(resolvedLayer);
295+
this.provider._position = this.props.position;
296+
if (newProps.options.opacity !== undefined) {
297+
this.provider.alpha = newProps.options.opacity;
298+
}
299+
this.props.onImageryLayersTreeUpdate();
300+
newProps.map.scene.requestRender();
301+
}
302+
});
303+
return;
304+
}
305+
267306
this.removeLayer();
268307
this.layer = newLayer;
269308
if (newProps.options.visibility) {
270309
this.addLayer(newProps);
271310
}
311+
272312
}
273313
this.updateZIndex(newProps.position);
274314
newProps.map.scene.requestRender();
@@ -278,6 +318,24 @@ class CesiumLayer extends React.Component {
278318
if (newProps.options.useForElevation) {
279319
this.props.map.terrainProvider = this.layer;
280320
} else {
321+
const isPromise = this.layer && isFunction(this.layer.then);
322+
if (isPromise) {
323+
this.layer.then((resolvedLayer) => {
324+
if (this._isMounted) {
325+
this.layer = resolvedLayer;
326+
327+
this.provider = this.props.map.imageryLayers.addImageryProvider(resolvedLayer);
328+
this.provider._position = this.props.position;
329+
if (newProps.options.opacity !== undefined) {
330+
this.provider.alpha = newProps.options.opacity;
331+
}
332+
this.props.onImageryLayersTreeUpdate();
333+
newProps.map.scene.requestRender();
334+
}
335+
});
336+
return;
337+
}
338+
281339
this.provider = this.props.map.imageryLayers.addImageryProvider(this.layer);
282340
this.provider._position = this.props.position;
283341
if (newProps.options.opacity !== undefined) {
@@ -323,7 +381,7 @@ class CesiumLayer extends React.Component {
323381
}
324382

325383
removeLayer = (provider) => {
326-
if (this.layer.destroy) {
384+
if (this.layer?.destroy) {
327385
this.layer.destroy();
328386
}
329387
const toRemove = provider || this.provider;
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/**
2+
* Copyright 2025, GeoSolutions Sas.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
import Layers from '../../../../utils/cesium/Layers';
10+
11+
import isEqual from 'lodash/isEqual';
12+
import proj4 from 'proj4';
13+
14+
/*
15+
only TIFFImageryProvider v2.17 is compatible for cesium v1.134 used now in MS
16+
*/
17+
import TIFFImageryProvider from 'tiff-imagery-provider';
18+
import { COG_LAYER_TYPE } from '../../../../utils/CatalogUtils';
19+
import {isProjectionAvailable} from '../../../../utils/ProjectionUtils';
20+
21+
/*
22+
colorScale set of values used by TIFFImageryProvider see https://observablehq.com/@d3/color-schemes
23+
*/
24+
function buildRenderOptions(options) {
25+
const band = options?.sources[0]?.band || 1;
26+
const nodata = Number(options.sources?.[0]?.nodata);
27+
const domain = [Number(options.sources?.[0]?.min), Number(options.sources?.[0]?.max)];
28+
return {
29+
band,
30+
single: {
31+
colorScale: 'greys',
32+
nodata,
33+
domain
34+
}
35+
};
36+
}
37+
38+
/*
39+
`projFunc` is experimental in tiff-imagery-provider
40+
*/
41+
const createLayer = (options) => {
42+
43+
if (!options.visibility) {
44+
return null;
45+
}
46+
const url = options.url || options?.sources[0]?.url;
47+
48+
return TIFFImageryProvider.fromUrl(url, {
49+
projFunc: (code) => {
50+
const epsgCode = `EPSG:${code}`;
51+
if (isProjectionAvailable(epsgCode)) {
52+
return {
53+
project: proj4(`EPSG:4326`, epsgCode).forward,
54+
unproject: proj4(`EPSG:4326`, epsgCode).inverse
55+
};
56+
}
57+
return null;
58+
},
59+
renderOptions: buildRenderOptions(options)
60+
});
61+
};
62+
63+
Layers.registerType(COG_LAYER_TYPE, {
64+
create: createLayer,
65+
update: (layer, newOptions, oldOptions) => {
66+
if (!isEqual(newOptions.sources, oldOptions.sources)) {
67+
return createLayer(newOptions);
68+
}
69+
return null;
70+
}
71+
});

web/client/components/map/cesium/plugins/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,6 @@ import './ModelLayer';
2222
import './ElevationLayer';
2323
import './ArcGISLayer';
2424
import './FlatGeobufLayer';
25+
import './COGLayer';
2526

2627
export default {};

web/client/plugins/TOC/components/DefaultLayer.jsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,6 @@ const getLayerVisibilityWarningMessageId = (node, config = {}) => {
3030
if (config.visualizationMode === VisualizationModes._2D && ['3dtiles', 'model'].includes(node.type)) {
3131
return 'toc.notVisibleSwitchTo3D';
3232
}
33-
if (config.visualizationMode === VisualizationModes._3D && ['cog'].includes(node.type)) {
34-
return 'toc.notVisibleSwitchTo2D';
35-
}
3633
if (config.resolution !== undefined && !isInsideResolutionsLimits(node, config.resolution)) {
3734
const maxResolution = node.maxResolution || Infinity;
3835
return config.resolution >= maxResolution

web/client/utils/cog/LayerUtils.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import get from 'lodash/get';
44

55
import { isProjectionAvailable } from "../ProjectionUtils";
66

7+
export const getTiffImageryProvider = () => import('tiff-imagery-provider').then(mod => mod);
8+
9+
710
let LayerUtils;
811

912
/**
@@ -47,7 +50,7 @@ export const fromUrl = (url, signal) => {
4750
return new Promise((resolve, reject) => {
4851
signal?.addEventListener("abort", () => abortError(reject));
4952
return fromGeotiffUrl(url)
50-
.then((image)=> image.getImage()) // Fetch and read first image to get medatadata of the tif
53+
.then((image)=> image.getImage())
5154
.then((image) => resolve(image))
5255
.catch(()=> abortError(reject));
5356
});
@@ -85,11 +88,10 @@ export const getLayerConfig = ({ url, layer, controller }) => {
8588
resolution: image.getResolution(),
8689
samples,
8790
fileDirectory: {
88-
// add more fileDirectory properties based on requirement
8991
PhotometricInterpretation: get(image, 'fileDirectory.PhotometricInterpretation')
9092
}
9193
},
92-
// skip adding bbox when geokeys or extent is empty
94+
9395
...(!isEmpty(extent) && !isEmpty(crs) && {
9496
bbox: {
9597
crs,
@@ -110,6 +112,7 @@ export const getLayerConfig = ({ url, layer, controller }) => {
110112
};
111113

112114
LayerUtils = {
115+
getTiffImageryProvider,
113116
getProjectionFromGeoKeys,
114117
fromUrl,
115118
getLayerConfig

0 commit comments

Comments
 (0)