Skip to content

Commit b73617b

Browse files
Fix geosolutions-it#11522 Support of identify for COG layers (geosolutions-it#11967)
1 parent 5939813 commit b73617b

9 files changed

Lines changed: 260 additions & 15 deletions

File tree

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ class CesiumLayer extends React.Component {
241241
if (isPromise) {
242242
this.layer.then((resolvedLayer) => {
243243
this.layer = resolvedLayer;
244+
this.layer.layerId = options.id;
244245
this.provider = map.imageryLayers.addImageryProvider(resolvedLayer);
245246
if (this.layer) {
246247
this.layer.layerName = options.name;

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

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
} from '../../../utils/MapUtils';
2626
import { reprojectBbox } from '../../../utils/CoordinatesUtils';
2727
import { throttle, isEqual, debounce } from 'lodash';
28+
import TIFFImageryProvider from 'tiff-imagery-provider';
2829

2930
class CesiumMap extends React.Component {
3031
static propTypes = {
@@ -265,7 +266,9 @@ class CesiumMap extends React.Component {
265266
onClick = (map, movement) => {
266267
if (this.props.onClick && movement.position !== null) {
267268
const cartesian = map.camera.pickEllipsoid(movement.position, map.scene.globe.ellipsoid);
269+
268270
const intersectedFeatures = this.getIntersectedFeatures(map, movement.position);
271+
269272
let cartographic = ClickUtils.getMouseXYZ(map, movement) || cartesian && Cesium.Cartographic.fromCartesian(cartesian);
270273
if (cartographic) {
271274
const latitude = cartographic.latitude * 180.0 / Math.PI;
@@ -284,23 +287,25 @@ class CesiumMap extends React.Component {
284287

285288
const y = (90.0 - latitude) / 180.0 * this.props.standardHeight * (this.props.zoom + 1);
286289
const x = (180.0 + longitude) / 360.0 * this.props.standardWidth * (this.props.zoom + 1);
287-
this.props.onClick({
288-
pixel: {
289-
x: x,
290-
y: y
291-
},
290+
const latlng = { lat: latitude, lng: longitude, z: elevation };
291+
const pixel = { x, y };
292+
const pointToBuildRequest = {
293+
pixel,
292294
height: (this.props.mapOptions && this.props.mapOptions.terrainProvider) || intersectedFeatures.length > 0
293295
? cartographic.height
294296
: undefined,
295297
cartographic,
296-
latlng: {
297-
lat: latitude,
298-
lng: longitude,
299-
z: elevation
300-
},
298+
latlng,
301299
crs: "EPSG:4326",
302300
intersectedFeatures,
303301
resolution: getResolutions()[Math.round(this.props.zoom)]
302+
};
303+
304+
this.getIntersectedPixels(map, {...movement.position, ...cartographic}).then(intersectedPixels => {
305+
306+
pointToBuildRequest.intersectedPixels = intersectedPixels;
307+
308+
this.props.onClick(pointToBuildRequest);
304309
});
305310
}
306311
}
@@ -389,6 +394,33 @@ class CesiumMap extends React.Component {
389394
return this.props.zoomToHeight / Math.pow(2, zoom - 1);
390395
};
391396

397+
/**
398+
* wrapper for TIFFImageryProvider pickFeatures() is async operation and we need append results and call onClick
399+
* https://github.com/hongfaqiu/TIFFImageryProvider/blob/v2.17.1/packages/TIFFImageryProvider/src/TIFFImageryProvider.ts#L768
400+
* @param {zoom} map
401+
* @param {x, y, longitude, latitude} position
402+
* @returns Array of layers with relative intersected pixels
403+
*/
404+
getIntersectedPixels = (map, position) => {
405+
406+
const tiffLayers = map.imageryLayers._layers.filter(layer =>
407+
layer.rendered &&
408+
layer.imageryProvider instanceof TIFFImageryProvider
409+
);
410+
411+
return Promise.all(tiffLayers.map(layer => {
412+
return layer.imageryProvider.pickFeatures(position.x, position.y, map.zoom, position.longitude, position.latitude)
413+
.then(pickedLayers => {
414+
const {data} = pickedLayers[0] || {};
415+
return {
416+
id: layer._imageryProvider.layerId,
417+
// remap bands index start from 1 instead of 0 to be consistent with 2D pick and avoid confusion with users
418+
bands: Object.fromEntries(Object.entries(data).map(([key, value]) => [Number(key) + 1, value]))
419+
};
420+
});
421+
}));
422+
}
423+
392424
getIntersectedFeatures = (map, position) => {
393425
// for consistency with 2D view we allow to drill pick through the first feature
394426
// and intersect all the features behind

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import proj4 from 'proj4';
1616
*/
1717
import TIFFImageryProvider from 'tiff-imagery-provider';
1818
import { COG_LAYER_TYPE } from '../../../../utils/CatalogUtils';
19-
import {isProjectionAvailable} from '../../../../utils/ProjectionUtils';
19+
import { isProjectionAvailable } from '../../../../utils/ProjectionUtils';
2020

2121
/*
2222
colorScale set of values used by TIFFImageryProvider see https://observablehq.com/@d3/color-schemes
@@ -56,7 +56,8 @@ const createLayer = (options) => {
5656
}
5757
return null;
5858
},
59-
renderOptions: buildRenderOptions(options)
59+
renderOptions: buildRenderOptions(options),
60+
enablePickFeatures: true // required for identify pickFeatures method
6061
});
6162
};
6263

web/client/components/map/openlayers/Map.jsx

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ import { DEFAULT_INTERACTION_OPTIONS } from '../../../utils/openlayers/DrawUtils
2828

2929
import {isEqual, find, throttle, isArray, isNil} from 'lodash';
3030

31+
import GeoTIFF from 'ol/source/GeoTIFF.js';
32+
3133
import 'ol/ol.css';
3234

3335
// add overrides for css
@@ -221,6 +223,9 @@ class OpenlayersMap extends React.Component {
221223
});
222224
const intersectedFeatures = this.getIntersectedFeatures(map, event?.pixel);
223225
const tLng = normalizeLng(coords.x);
226+
227+
const intersectedPixels = this.getIntersectedPixels(map, event?.pixel);
228+
224229
this.props.onClick({
225230
pixel: {
226231
x: event.pixel[0],
@@ -238,7 +243,8 @@ class OpenlayersMap extends React.Component {
238243
metaKey: event.originalEvent.metaKey, // MAC OS
239244
shift: event.originalEvent.shiftKey
240245
},
241-
intersectedFeatures
246+
intersectedFeatures,
247+
intersectedPixels
242248
}, layerInfo);
243249
}
244250
}
@@ -379,6 +385,35 @@ class OpenlayersMap extends React.Component {
379385
return view.getProjection().getExtent() || msGetProjection(props.projection).extent;
380386
};
381387

388+
/**
389+
*
390+
* @param {zoom} map
391+
* @param {x, y, longitude, latitude} position
392+
* @returns Array of layers with relative intersected pixels
393+
*/
394+
getIntersectedPixels = (map, position) => {
395+
396+
const allLayers = map.getLayers().getArray();
397+
398+
const tiffLayers = allLayers.filter(layer =>
399+
layer.rendered &&
400+
layer.getSource() instanceof GeoTIFF
401+
);
402+
403+
const result = tiffLayers.map(layer => {
404+
const rawdata = layer.getData(position);
405+
if (!rawdata) return null;
406+
const data = Array.from(rawdata);
407+
// const source = layer.getSource();
408+
return {
409+
id: layer.get('msId'),
410+
// remap bands index start from 1 instead of 0 to be consistent with 2D pick and avoid confusion with users
411+
bands: data.reduce((acc, value, index) => ({ ...acc, [index + 1]: value }), {})
412+
};
413+
}).filter(val => val !== null);
414+
return result;
415+
}
416+
382417
getIntersectedFeatures = (map, pixel) => {
383418
let groupIntersectedFeatures = {};
384419
map.forEachFeatureAtPixel(pixel, (feature, layer) => {

web/client/components/map/openlayers/plugins/COGLayer.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ function create(options) {
2424
sourceOptions.headers = requestConfig.headers;
2525
}
2626
}
27-
return new TileLayer({
27+
const layerOl = new TileLayer({
2828
msId: options.id,
2929
style: get(options, 'style.body'),
3030
opacity: options.opacity !== undefined ? options.opacity : 1,
@@ -35,10 +35,13 @@ function create(options) {
3535
wrapX: true,
3636
sourceOptions
3737
}),
38+
enablePickFeatures: true,
3839
zIndex: options.zIndex,
3940
minResolution: options.minResolution,
4041
maxResolution: options.maxResolution
4142
});
43+
44+
return layerOl;
4245
}
4346

4447
Layers.registerType('cog', {

web/client/utils/MapInfoUtils.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import vector from './mapinfo/vector';
2222
import threeDTiles from './mapinfo/threeDTiles';
2323
import model from './mapinfo/model';
2424
import arcgis from './mapinfo/arcgis';
25+
import cog from './mapinfo/cog';
2526
import flatgeobuf from './mapinfo/flatgeobuf';
2627
// TODO import only index in ./mapinfo
2728

@@ -377,7 +378,8 @@ export const services = {
377378
'3dtiles': threeDTiles,
378379
'model': model,
379380
'arcgis': arcgis,
380-
'flatgeobuf': flatgeobuf
381+
'flatgeobuf': flatgeobuf,
382+
'cog': cog
381383
};
382384
/**
383385
* To get the custom viewer with the given type

web/client/utils/cog/LayerUtils.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,3 +119,4 @@ LayerUtils = {
119119
};
120120

121121
export default LayerUtils;
122+
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/*
2+
* Copyright 2026, 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 expect from "expect";
10+
import cog from "../cog";
11+
12+
describe("mapinfo COG utils", () => {
13+
it("should create a getFeatureInfo request", () => {
14+
const layerId = "6ba42670-f3c-21f0-8e1f-dd66f6ae634d";
15+
const layer = {
16+
"id": layerId,
17+
"format": "cog",
18+
"title": {
19+
"en-US": "Cloud layer title"
20+
},
21+
"type": "cog",
22+
"url": "https://mydomain.com/cog.tif",
23+
"visibility": true,
24+
"singleTile": false,
25+
"dimensions": [],
26+
"hideLoading": false,
27+
"handleClickOnLayer": false,
28+
"useForElevation": false,
29+
"hidden": false,
30+
"expanded": false
31+
};
32+
const currentLocale = "en-US";
33+
const pixValueRaw = new Uint8Array([140, 80, 80, 255]);
34+
const pixValueBands = pixValueRaw.reduce((acc, value, index) => ({ ...acc, [index + 1]: value }), {});
35+
const latlng = {
36+
"lat": 40.19133465092119, "lng": -92.60925292968749
37+
};
38+
const point = {
39+
"pixel": {
40+
"x": 460, "y": 110
41+
},
42+
"latlng": latlng,
43+
"rawPos": [
44+
-92.60925292968749, 40.19133465092119
45+
],
46+
"modifiers": {
47+
"alt": false,
48+
"ctrl": false,
49+
"metaKey": false,
50+
"shift": false
51+
},
52+
"intersectedPixels": {
53+
"0": {
54+
"id": layerId,
55+
"bands": pixValueBands
56+
}
57+
},
58+
"intersectedFeatures": [
59+
{
60+
"id": layerId,
61+
"features": [
62+
{
63+
"id": 165,
64+
"properties": {
65+
"id": "USA",
66+
"name": "United States of America"
67+
},
68+
"type": "Feature",
69+
"geometry": {
70+
"type": "MultiPolygon",
71+
"coordinates": [
72+
[
73+
[
74+
[-159.34512, 21.982],
75+
[-159.46372, 21.88299],
76+
[-159.80051, 22.06533],
77+
[-159.34512, 21.982]
78+
]
79+
]
80+
]
81+
}
82+
}
83+
]
84+
}
85+
]
86+
};
87+
88+
const request = cog.buildRequest(layer, { point, currentLocale });
89+
const expectedRequest = {
90+
"request": {
91+
"features": [
92+
{
93+
"type": "Feature",
94+
"geometry": {
95+
"type": "Point",
96+
"coordinates": [latlng.lng, latlng.lat]
97+
},
98+
"properties": {
99+
"band 1": pixValueRaw[0],
100+
"band 2": pixValueRaw[1],
101+
"band 3": pixValueRaw[2],
102+
"band 4": pixValueRaw[3]
103+
}
104+
}
105+
],
106+
"outputFormat": "application/json"
107+
},
108+
"metadata": {
109+
"title": "Cloud layer title"
110+
},
111+
"url": "https://mydomain.com/cog.tif"
112+
};
113+
114+
expect(request).toEqual(expectedRequest);
115+
});
116+
});

web/client/utils/mapinfo/cog.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* Copyright 2026, 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 { Observable } from 'rxjs';
10+
import isObject from 'lodash/isObject';
11+
12+
export default {
13+
buildRequest: (layer, { point, currentLocale } = {}) => { // executed for each COG layer in TOC
14+
15+
const pickValues = Object.values(point?.intersectedPixels);
16+
const arrayValues = pickValues ? Array.from(pickValues) : [];
17+
const filteredValues = arrayValues.filter(({ id }) => id === layer.id);
18+
19+
const features = filteredValues.map((value) => ({
20+
type: 'Feature',
21+
geometry: {
22+
type: 'Point',
23+
coordinates: [point.latlng.lng, point.latlng.lat]
24+
},
25+
properties: value?.bands ?
26+
Object.entries(value.bands).reduce((acc, [key, val]) => {
27+
acc[`band ${key}`] = val;
28+
return acc;
29+
}, {})
30+
: {}
31+
}));
32+
33+
return {
34+
request: {
35+
features: [...features],
36+
outputFormat: 'application/json'
37+
},
38+
metadata: {
39+
title: isObject(layer.title)
40+
? layer.title[currentLocale] || layer.title.default
41+
: layer.title
42+
},
43+
url: layer.url
44+
};
45+
},
46+
getIdentifyFlow: (layer, basePath, {features = []} = {}) => {
47+
48+
return Observable.of({
49+
data: {
50+
features
51+
}
52+
});
53+
}
54+
};

0 commit comments

Comments
 (0)