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
1 change: 1 addition & 0 deletions web/client/components/map/cesium/Layer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ class CesiumLayer extends React.Component {
if (isPromise) {
this.layer.then((resolvedLayer) => {
this.layer = resolvedLayer;
this.layer.layerId = options.id;
this.provider = map.imageryLayers.addImageryProvider(resolvedLayer);
if (this.layer) {
this.layer.layerName = options.name;
Expand Down
52 changes: 42 additions & 10 deletions web/client/components/map/cesium/Map.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
} from '../../../utils/MapUtils';
import { reprojectBbox } from '../../../utils/CoordinatesUtils';
import { throttle, isEqual, debounce } from 'lodash';
import TIFFImageryProvider from 'tiff-imagery-provider';

class CesiumMap extends React.Component {
static propTypes = {
Expand Down Expand Up @@ -265,7 +266,9 @@ class CesiumMap extends React.Component {
onClick = (map, movement) => {
if (this.props.onClick && movement.position !== null) {
const cartesian = map.camera.pickEllipsoid(movement.position, map.scene.globe.ellipsoid);

const intersectedFeatures = this.getIntersectedFeatures(map, movement.position);

let cartographic = ClickUtils.getMouseXYZ(map, movement) || cartesian && Cesium.Cartographic.fromCartesian(cartesian);
if (cartographic) {
const latitude = cartographic.latitude * 180.0 / Math.PI;
Expand All @@ -284,23 +287,25 @@ class CesiumMap extends React.Component {

const y = (90.0 - latitude) / 180.0 * this.props.standardHeight * (this.props.zoom + 1);
const x = (180.0 + longitude) / 360.0 * this.props.standardWidth * (this.props.zoom + 1);
this.props.onClick({
pixel: {
x: x,
y: y
},
const latlng = { lat: latitude, lng: longitude, z: elevation };
const pixel = { x, y };
const pointToBuildRequest = {
pixel,
height: (this.props.mapOptions && this.props.mapOptions.terrainProvider) || intersectedFeatures.length > 0
? cartographic.height
: undefined,
cartographic,
latlng: {
lat: latitude,
lng: longitude,
z: elevation
},
latlng,
crs: "EPSG:4326",
intersectedFeatures,
resolution: getResolutions()[Math.round(this.props.zoom)]
};

this.getIntersectedPixels(map, {...movement.position, ...cartographic}).then(intersectedPixels => {

pointToBuildRequest.intersectedPixels = intersectedPixels;

this.props.onClick(pointToBuildRequest);
});
}
}
Expand Down Expand Up @@ -389,6 +394,33 @@ class CesiumMap extends React.Component {
return this.props.zoomToHeight / Math.pow(2, zoom - 1);
};

/**
* wrapper for TIFFImageryProvider pickFeatures() is async operation and we need append results and call onClick
* https://github.com/hongfaqiu/TIFFImageryProvider/blob/v2.17.1/packages/TIFFImageryProvider/src/TIFFImageryProvider.ts#L768
* @param {zoom} map
* @param {x, y, longitude, latitude} position
* @returns Array of layers with relative intersected pixels
*/
getIntersectedPixels = (map, position) => {

const tiffLayers = map.imageryLayers._layers.filter(layer =>
layer.rendered &&
layer.imageryProvider instanceof TIFFImageryProvider
);

return Promise.all(tiffLayers.map(layer => {
return layer.imageryProvider.pickFeatures(position.x, position.y, map.zoom, position.longitude, position.latitude)
.then(pickedLayers => {
const {data} = pickedLayers[0] || {};
return {
id: layer._imageryProvider.layerId,
// remap bands index start from 1 instead of 0 to be consistent with 2D pick and avoid confusion with users
bands: Object.fromEntries(Object.entries(data).map(([key, value]) => [Number(key) + 1, value]))
};
});
}));
}

getIntersectedFeatures = (map, position) => {
// for consistency with 2D view we allow to drill pick through the first feature
// and intersect all the features behind
Expand Down
5 changes: 3 additions & 2 deletions web/client/components/map/cesium/plugins/COGLayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import proj4 from 'proj4';
*/
import TIFFImageryProvider from 'tiff-imagery-provider';
import { COG_LAYER_TYPE } from '../../../../utils/CatalogUtils';
import {isProjectionAvailable} from '../../../../utils/ProjectionUtils';
import { isProjectionAvailable } from '../../../../utils/ProjectionUtils';

/*
colorScale set of values used by TIFFImageryProvider see https://observablehq.com/@d3/color-schemes
Expand Down Expand Up @@ -56,7 +56,8 @@ const createLayer = (options) => {
}
return null;
},
renderOptions: buildRenderOptions(options)
renderOptions: buildRenderOptions(options),
enablePickFeatures: true // required for identify pickFeatures method
});
};

Expand Down
37 changes: 36 additions & 1 deletion web/client/components/map/openlayers/Map.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import { DEFAULT_INTERACTION_OPTIONS } from '../../../utils/openlayers/DrawUtils

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

import GeoTIFF from 'ol/source/GeoTIFF.js';

import 'ol/ol.css';

// add overrides for css
Expand Down Expand Up @@ -221,6 +223,9 @@ class OpenlayersMap extends React.Component {
});
const intersectedFeatures = this.getIntersectedFeatures(map, event?.pixel);
const tLng = normalizeLng(coords.x);

const intersectedPixels = this.getIntersectedPixels(map, event?.pixel);

this.props.onClick({
pixel: {
x: event.pixel[0],
Expand All @@ -238,7 +243,8 @@ class OpenlayersMap extends React.Component {
metaKey: event.originalEvent.metaKey, // MAC OS
shift: event.originalEvent.shiftKey
},
intersectedFeatures
intersectedFeatures,
intersectedPixels
}, layerInfo);
}
}
Expand Down Expand Up @@ -379,6 +385,35 @@ class OpenlayersMap extends React.Component {
return view.getProjection().getExtent() || msGetProjection(props.projection).extent;
};

/**
*
* @param {zoom} map
* @param {x, y, longitude, latitude} position
* @returns Array of layers with relative intersected pixels
*/
getIntersectedPixels = (map, position) => {

const allLayers = map.getLayers().getArray();

const tiffLayers = allLayers.filter(layer =>
layer.rendered &&
layer.getSource() instanceof GeoTIFF
);

const result = tiffLayers.map(layer => {
const rawdata = layer.getData(position);
if (!rawdata) return null;
const data = Array.from(rawdata);
// const source = layer.getSource();
return {
id: layer.get('msId'),
// remap bands index start from 1 instead of 0 to be consistent with 2D pick and avoid confusion with users
bands: data.reduce((acc, value, index) => ({ ...acc, [index + 1]: value }), {})
};
}).filter(val => val !== null);
return result;
}

getIntersectedFeatures = (map, pixel) => {
let groupIntersectedFeatures = {};
map.forEachFeatureAtPixel(pixel, (feature, layer) => {
Expand Down
5 changes: 4 additions & 1 deletion web/client/components/map/openlayers/plugins/COGLayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ function create(options) {
sourceOptions.headers = requestConfig.headers;
}
}
return new TileLayer({
const layerOl = new TileLayer({
msId: options.id,
style: get(options, 'style.body'),
opacity: options.opacity !== undefined ? options.opacity : 1,
Expand All @@ -35,10 +35,13 @@ function create(options) {
wrapX: true,
sourceOptions
}),
enablePickFeatures: true,
zIndex: options.zIndex,
minResolution: options.minResolution,
maxResolution: options.maxResolution
});

return layerOl;
}

Layers.registerType('cog', {
Expand Down
4 changes: 3 additions & 1 deletion web/client/utils/MapInfoUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import vector from './mapinfo/vector';
import threeDTiles from './mapinfo/threeDTiles';
import model from './mapinfo/model';
import arcgis from './mapinfo/arcgis';
import cog from './mapinfo/cog';
import flatgeobuf from './mapinfo/flatgeobuf';
// TODO import only index in ./mapinfo

Expand Down Expand Up @@ -377,7 +378,8 @@ export const services = {
'3dtiles': threeDTiles,
'model': model,
'arcgis': arcgis,
'flatgeobuf': flatgeobuf
'flatgeobuf': flatgeobuf,
'cog': cog
};
/**
* To get the custom viewer with the given type
Expand Down
1 change: 1 addition & 0 deletions web/client/utils/cog/LayerUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,4 @@ LayerUtils = {
};

export default LayerUtils;

116 changes: 116 additions & 0 deletions web/client/utils/mapinfo/__tests__/cog-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
* Copyright 2026, 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 cog from "../cog";

describe("mapinfo COG utils", () => {
it("should create a getFeatureInfo request", () => {
const layerId = "6ba42670-f3c-21f0-8e1f-dd66f6ae634d";
const layer = {
"id": layerId,
"format": "cog",
"title": {
"en-US": "Cloud layer title"
},
"type": "cog",
"url": "https://mydomain.com/cog.tif",
"visibility": true,
"singleTile": false,
"dimensions": [],
"hideLoading": false,
"handleClickOnLayer": false,
"useForElevation": false,
"hidden": false,
"expanded": false
};
const currentLocale = "en-US";
const pixValueRaw = new Uint8Array([140, 80, 80, 255]);
const pixValueBands = pixValueRaw.reduce((acc, value, index) => ({ ...acc, [index + 1]: value }), {});
const latlng = {
"lat": 40.19133465092119, "lng": -92.60925292968749
};
const point = {
"pixel": {
"x": 460, "y": 110
},
"latlng": latlng,
"rawPos": [
-92.60925292968749, 40.19133465092119
],
"modifiers": {
"alt": false,
"ctrl": false,
"metaKey": false,
"shift": false
},
"intersectedPixels": {
"0": {
"id": layerId,
"bands": pixValueBands
}
},
"intersectedFeatures": [
{
"id": layerId,
"features": [
{
"id": 165,
"properties": {
"id": "USA",
"name": "United States of America"
},
"type": "Feature",
"geometry": {
"type": "MultiPolygon",
"coordinates": [
[
[
[-159.34512, 21.982],
[-159.46372, 21.88299],
[-159.80051, 22.06533],
[-159.34512, 21.982]
]
]
]
}
}
]
}
]
};

const request = cog.buildRequest(layer, { point, currentLocale });
const expectedRequest = {
"request": {
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [latlng.lng, latlng.lat]
},
"properties": {
"band 1": pixValueRaw[0],
"band 2": pixValueRaw[1],
"band 3": pixValueRaw[2],
"band 4": pixValueRaw[3]
}
}
],
"outputFormat": "application/json"
},
"metadata": {
"title": "Cloud layer title"
},
"url": "https://mydomain.com/cog.tif"
};

expect(request).toEqual(expectedRequest);
});
});
54 changes: 54 additions & 0 deletions web/client/utils/mapinfo/cog.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* Copyright 2026, 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 { Observable } from 'rxjs';
import isObject from 'lodash/isObject';

export default {
buildRequest: (layer, { point, currentLocale } = {}) => { // executed for each COG layer in TOC

const pickValues = Object.values(point?.intersectedPixels);
const arrayValues = pickValues ? Array.from(pickValues) : [];
const filteredValues = arrayValues.filter(({ id }) => id === layer.id);

const features = filteredValues.map((value) => ({
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [point.latlng.lng, point.latlng.lat]
},
properties: value?.bands ?
Object.entries(value.bands).reduce((acc, [key, val]) => {
acc[`band ${key}`] = val;
return acc;
}, {})
: {}
}));

return {
request: {
features: [...features],
outputFormat: 'application/json'
},
metadata: {
title: isObject(layer.title)
? layer.title[currentLocale] || layer.title.default
: layer.title
},
url: layer.url
};
},
getIdentifyFlow: (layer, basePath, {features = []} = {}) => {

return Observable.of({
data: {
features
}
});
}
};
Loading