From 0d694f1c487ca9e007cc31764bfe51e560652ffa Mon Sep 17 00:00:00 2001 From: rowheat02 Date: Fri, 29 May 2026 16:04:07 +0545 Subject: [PATCH] pixel value for 3d --- web/client/components/map/cesium/Map.jsx | 38 +++++---- web/client/components/map/openlayers/Map.jsx | 4 +- web/client/utils/cog/IdentifyUtils.js | 60 ++++++++++++++ .../utils/cog/__tests__/IdentifyUtils-test.js | 61 ++++++++++++++ .../utils/mapinfo/__tests__/cog-test.js | 83 ++++++++++++++----- web/client/utils/mapinfo/cog.js | 59 ++++++++----- 6 files changed, 242 insertions(+), 63 deletions(-) create mode 100644 web/client/utils/cog/IdentifyUtils.js create mode 100644 web/client/utils/cog/__tests__/IdentifyUtils-test.js diff --git a/web/client/components/map/cesium/Map.jsx b/web/client/components/map/cesium/Map.jsx index b7c9961da0e..f6bf3737d01 100644 --- a/web/client/components/map/cesium/Map.jsx +++ b/web/client/components/map/cesium/Map.jsx @@ -27,6 +27,7 @@ import { import { reprojectBbox } from '../../../utils/CoordinatesUtils'; import { throttle, isEqual, debounce } from 'lodash'; import TIFFImageryProvider from 'tiff-imagery-provider'; +import { getCOGPixelData } from '../../../utils/cog/IdentifyUtils'; class CesiumMap extends React.Component { static propTypes = { @@ -304,12 +305,8 @@ class CesiumMap extends React.Component { resolution: getResolutions()[Math.round(this.props.zoom)] }; - this.getIntersectedPixels(map, {...movement.position, ...cartographic}).then(intersectedPixels => { - - pointToBuildRequest.intersectedPixels = intersectedPixels; - - this.props.onClick(pointToBuildRequest); - }); + pointToBuildRequest.intersectedPixelsPromise = this.getIntersectedPixels(map, cartographic); + this.props.onClick(pointToBuildRequest); } } }; @@ -398,30 +395,35 @@ class CesiumMap extends React.Component { }; /** - * 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 + * Resolve COG pixel values for visible Cesium TIFF imagery layers. + * @param {object} map Cesium viewer + * @param {object} position cartographic click position + * @returns {Promise} layers with relative intersected pixels */ getIntersectedPixels = (map, position) => { const tiffLayers = map.imageryLayers._layers.filter(layer => - layer.rendered && + layer.show && 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 Promise.all(tiffLayers.map(layer => + getCOGPixelData({ + provider: layer.imageryProvider, + position, + zoom: this.props.zoom + }) + .then(data => { + if (!data) { + return null; + } 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])) }; - }); - })); + }) + )).then(intersectedPixels => intersectedPixels.filter(Boolean)); } getIntersectedFeatures = (map, position) => { diff --git a/web/client/components/map/openlayers/Map.jsx b/web/client/components/map/openlayers/Map.jsx index 7b7d0883eb7..7bdc2dd913b 100644 --- a/web/client/components/map/openlayers/Map.jsx +++ b/web/client/components/map/openlayers/Map.jsx @@ -203,7 +203,7 @@ class OpenlayersMap extends React.Component { const finalLat = markerCoords ? markerCoords.y : lat; const finalLng = markerCoords ? normalizeLng(markerCoords.x) : lng; const intersectedFeatures = this.getIntersectedFeatures(map, event?.pixel); - const intersectedPixels = this.getIntersectedPixels(map, event?.pixel); + const intersectedPixelsPromise = Promise.resolve(this.getIntersectedPixels(map, event?.pixel)); this.props.onClick({ pixel: { @@ -223,7 +223,7 @@ class OpenlayersMap extends React.Component { shift: event.originalEvent.shiftKey }, intersectedFeatures, - intersectedPixels + intersectedPixelsPromise }, layerInfo); } }); diff --git a/web/client/utils/cog/IdentifyUtils.js b/web/client/utils/cog/IdentifyUtils.js new file mode 100644 index 00000000000..42adc4235cf --- /dev/null +++ b/web/client/utils/cog/IdentifyUtils.js @@ -0,0 +1,60 @@ +/** + * 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 * as Cesium from 'cesium'; + +/** + * Convert a cartographic position to a pixel index inside loaded COG tile data. + * The click is converted to a ratio inside the Cesium tile rectangle. + * + */ +export const getPixelFromTileData = ({ data, width, height, rectangle, position }) => { + if (!data || !width || !height) { + return null; + } + // Locate the click as a ratio inside the Cesium tile rectangle. + const xRatio = (position.longitude - rectangle.west) / (rectangle.east - rectangle.west); + const yRatio = (rectangle.north - position.latitude) / (rectangle.north - rectangle.south); + // Pixel column in the loaded tile data. + const x = Math.max(0, Math.min(width - 1, Math.floor(xRatio * width))); + // Pixel row in the loaded tile data. + const y = Math.max(0, Math.min(height - 1, Math.floor(yRatio * height))); + // Flat array index used by each band array. + const index = y * width + x; + return Object.fromEntries(data.map((value, idx) => [idx, value?.[index]])); +}; + +/** + * Load the COG tile covering the click position and extract its pixel values. + * Cesium resolves the tile from the click coordinate; the provider loads the + * raster arrays for that tile and getPixelFromTileData reads the band values. + */ +export const getCOGPixelData = ({ provider, position, zoom }) => { + if (!provider._loadTile || !provider.tilingScheme?.positionToTileXY) { + return Promise.resolve(null); + } + // Use the closest available provider level for the current map zoom. + const maximumLevel = Math.max(0, (provider.requestLevels?.length || 1) - 1); + const tileZoom = Math.max(provider.minimumLevel || 0, Math.min(maximumLevel, Math.round(zoom))); + const cartographic = new Cesium.Cartographic(position.longitude, position.latitude); + // Find the Cesium imagery tile that contains the clicked coordinate. + const tile = provider.tilingScheme.positionToTileXY(cartographic, tileZoom); + if (!tile) { + return Promise.resolve(null); + } + const rectangle = provider.tilingScheme.tileXYToRectangle(tile.x, tile.y, tileZoom); + // Load the tile raster arrays and read the clicked pixel from each band. + return provider._loadTile(tile.x, tile.y, tileZoom) + .then(({ data, width, height }) => + getPixelFromTileData({ data, width, height, rectangle, position })); +}; + +export default { + getCOGPixelData, + getPixelFromTileData +}; diff --git a/web/client/utils/cog/__tests__/IdentifyUtils-test.js b/web/client/utils/cog/__tests__/IdentifyUtils-test.js new file mode 100644 index 00000000000..fead2af3490 --- /dev/null +++ b/web/client/utils/cog/__tests__/IdentifyUtils-test.js @@ -0,0 +1,61 @@ +/* + * 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 { getCOGPixelData } from '../IdentifyUtils'; + +describe('COG - IdentifyUtils', () => { + it('should get pixel values from a COG provider tile', (done) => { + const data = [ + [10, 11, 12, 13, 14, 15], + [20, 21, 22, 23, 24, 25], + [30, 31, 32, 33, 34, 35] + ]; + const provider = { + requestLevels: [0, 1, 2], + minimumLevel: 0, + tilingScheme: { + positionToTileXY: () => ({ x: 1, y: 2 }), + tileXYToRectangle: () => ({ + west: 0, + east: 10, + north: 10, + south: 0 + }) + }, + _loadTile: (x, y, zoom) => { + expect(x).toBe(1); + expect(y).toBe(2); + expect(zoom).toBe(2); + return Promise.resolve({ + data, + width: 3, + height: 2 + }); + } + }; + + getCOGPixelData({ + provider, + position: { + longitude: 5, + latitude: 5 + }, + zoom: 2 + }) + .then(result => { + expect(result).toEqual({ + 0: 14, + 1: 24, + 2: 34 + }); + done(); + }) + .catch(done); + }); +}); diff --git a/web/client/utils/mapinfo/__tests__/cog-test.js b/web/client/utils/mapinfo/__tests__/cog-test.js index e45e3e04ab3..f94f0953c37 100644 --- a/web/client/utils/mapinfo/__tests__/cog-test.js +++ b/web/client/utils/mapinfo/__tests__/cog-test.js @@ -32,6 +32,12 @@ describe("mapinfo COG utils", () => { const currentLocale = "en-US"; const pixValueRaw = new Uint8Array([140, 80, 80, 255]); const pixValueBands = pixValueRaw.reduce((acc, value, index) => ({ ...acc, [index + 1]: value }), {}); + const intersectedPixelsPromise = Promise.resolve({ + "0": { + "id": layerId, + "bands": pixValueBands + } + }); const latlng = { "lat": 40.19133465092119, "lng": -92.60925292968749 }; @@ -49,12 +55,7 @@ describe("mapinfo COG utils", () => { "metaKey": false, "shift": false }, - "intersectedPixels": { - "0": { - "id": layerId, - "bands": pixValueBands - } - }, + intersectedPixelsPromise, "intersectedFeatures": [ { "id": layerId, @@ -88,22 +89,12 @@ describe("mapinfo COG utils", () => { 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" + "features": [], + "outputFormat": "application/json", + intersectedPixelsPromise, + point: { + latlng + } }, "metadata": { "title": "Cloud layer title" @@ -114,6 +105,54 @@ describe("mapinfo COG utils", () => { expect(request).toEqual(expectedRequest); }); + it("should create features from intersected pixels promise", (done) => { + const layerId = "6ba42670-f3c-21f0-8e1f-dd66f6ae634d"; + const layer = { id: layerId }; + const latlng = { + "lat": 40.19133465092119, "lng": -92.60925292968749 + }; + const intersectedPixelsPromise = Promise.resolve({ + "0": { + "id": layerId, + "bands": { + "1": 140, + "2": 80, + "3": 80, + "4": 255 + } + } + }); + + cog.getIdentifyFlow(layer, undefined, { + intersectedPixelsPromise, + point: { + latlng + } + }) + .toPromise() + .then((response) => { + expect(response).toEqual({ + data: { + features: [{ + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [latlng.lng, latlng.lat] + }, + "properties": { + "band 1": 140, + "band 2": 80, + "band 3": 80, + "band 4": 255 + } + }] + } + }); + done(); + }) + .catch(done); + }); + it("should use sources[0].url as fallback when layer.url is not set", () => { const layerId = "6ba42670-f3c-21f0-8e1f-dd66f6ae634d"; const sourceUrl = "https://mydomain.com/cog.tif"; diff --git a/web/client/utils/mapinfo/cog.js b/web/client/utils/mapinfo/cog.js index dcb5a445a02..8674f7c7015 100644 --- a/web/client/utils/mapinfo/cog.js +++ b/web/client/utils/mapinfo/cog.js @@ -12,28 +12,14 @@ 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' + features: [], + outputFormat: 'application/json', + intersectedPixelsPromise: point?.intersectedPixelsPromise, + point: { + latlng: point?.latlng + } }, metadata: { title: isObject(layer.title) @@ -43,7 +29,38 @@ export default { url: layer.url || layer?.sources?.[0]?.url }; }, - getIdentifyFlow: (layer, basePath, {features = []} = {}) => { + getIdentifyFlow: (layer, _, {features = [], intersectedPixelsPromise, point} = {}) => { + + if (intersectedPixelsPromise && point) { + return Observable.fromPromise(intersectedPixelsPromise) + .map((intersectedPixels = []) => { + const pickValues = Object.values(intersectedPixels); + const arrayValues = pickValues ? Array.from(pickValues) : []; + const filteredValues = arrayValues.filter(({ id }) => id === layer.id); + return { + data: { + 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; + }, {}) + : {} + })) + } + }; + }) + .catch(() => Observable.of({ + data: { + features: [] + } + })); + } return Observable.of({ data: {