Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#9588: Add support to multi-band color mapping for COG layers #9857

Merged
merged 11 commits into from
Mar 21, 2024
13 changes: 10 additions & 3 deletions docs/developer-guide/maps-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -1292,9 +1292,16 @@ i.e.
"visibility": false,
"name": "Name",
"sources": [
{ "url": "https://host-sample/cog1.tif" },
{ "url": "https://host-sample/cog2.tif" }
]
{ "url": "https://host-sample/cog1.tif", min: 1, max: 100, nodata: 0},
{ "url": "https://host-sample/cog2.tif", min: 1, max: 100, nodata: 255}
],
"style": {
"body": { // cog style currently supports only RGB with alpha band or single/gray band
"color": ["array", ["band", 1], ["band", 2], ["band", 3], ["band", 4]] // RGB with alpha band
// "color": ["array", ["band", 1], ["band", 1], ["band", 1], ["band", 2]] - single/gray band
},
"format": "openlayers",
}
}
```

Expand Down
98 changes: 12 additions & 86 deletions web/client/api/catalog/COG.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,13 @@
*/

import get from 'lodash/get';
import isEmpty from 'lodash/isEmpty';
import isNil from 'lodash/isNil';
import { Observable } from 'rxjs';
import { fromUrl as fromGeotiffUrl } from 'geotiff';


import { isValidURL } from '../../utils/URLUtils';
import ConfigUtils from '../../utils/ConfigUtils';
import { isProjectionAvailable } from '../../utils/ProjectionUtils';
import LayerUtils from '../../utils/cog/LayerUtils';
import { COG_LAYER_TYPE } from '../../utils/CatalogUtils';

const searchAndPaginate = (layers, startPosition, maxRecords, text) => {
Expand All @@ -31,51 +30,7 @@ const searchAndPaginate = (layers, startPosition, maxRecords, text) => {
records
};
};
/**
* Get projection code from geokeys
* @param {Object} image
* @returns {string} projection code
*/
export const getProjectionFromGeoKeys = (image) => {
const geoKeys = image.geoKeys;
if (!geoKeys) {
return null;
}

if (
geoKeys.ProjectedCSTypeGeoKey &&
geoKeys.ProjectedCSTypeGeoKey !== 32767
) {
return "EPSG:" + geoKeys.ProjectedCSTypeGeoKey;
}

if (
geoKeys.GeographicTypeGeoKey &&
geoKeys.GeographicTypeGeoKey !== 32767
) {
return "EPSG:" + geoKeys.GeographicTypeGeoKey;
}

return null;
};
const abortError = (reject) => reject(new DOMException("Aborted", "AbortError"));
/**
* fromUrl with abort fetching of data and data slices
* Note: The abort action will not cancel data fetch request but just the promise,
* because of the issue in https://github.com/geotiffjs/geotiff.js/issues/408
*/
const fromUrl = (url, signal) => {
if (signal?.aborted) {
return abortError(Promise.reject);
}
return new Promise((resolve, reject) => {
signal?.addEventListener("abort", () => abortError(reject));
return fromGeotiffUrl(url)
.then((image)=> image.getImage()) // Fetch and read first image to get medatadata of the tif
.then((image) => resolve(image))
.catch(()=> abortError(reject));
});
};
let capabilitiesCache = {};
export const getRecords = (_url, startPosition, maxRecords, text, info = {}) => {
const service = get(info, 'options.service');
Expand All @@ -93,50 +48,21 @@ export const getRecords = (_url, startPosition, maxRecords, text, info = {}) =>
};
const controller = get(info, 'options.controller');
const isSave = get(info, 'options.save', false);
const cached = capabilitiesCache[url];
if (cached && new Date().getTime() < cached.timestamp + (ConfigUtils.getConfigProp('cacheExpire') || 60) * 1000) {
return {...cached.data};
}
// Fetch metadata only on saving the service (skip on search)
if ((isNil(service.fetchMetadata) || service.fetchMetadata) && isSave) {
const cached = capabilitiesCache[url];
if (cached && new Date().getTime() < cached.timestamp + (ConfigUtils.getConfigProp('cacheExpire') || 60) * 1000) {
return {...cached.data};
}
return fromUrl(url, controller?.signal)
.then(image => {
const crs = getProjectionFromGeoKeys(image);
const extent = image.getBoundingBox();
const isProjectionDefined = isProjectionAvailable(crs);
layer = {
...layer,
sourceMetadata: {
crs,
extent: extent,
width: image.getWidth(),
height: image.getHeight(),
tileWidth: image.getTileWidth(),
tileHeight: image.getTileHeight(),
origin: image.getOrigin(),
resolution: image.getResolution()
},
// skip adding bbox when geokeys or extent is empty
...(!isEmpty(extent) && !isEmpty(crs) && {
bbox: {
crs,
...(isProjectionDefined && {
bounds: {
minx: extent[0],
miny: extent[1],
maxx: extent[2],
maxy: extent[3]
}}
)
}
})
};
return LayerUtils.getLayerConfig({url, controller, layer})
.then(updatedLayer => {
capabilitiesCache[url] = {
timestamp: new Date().getTime(),
data: {...layer}
data: {...updatedLayer}
};
return layer;
}).catch(() => ({...layer}));
return updatedLayer;
})
.catch(() => ({...layer}));
}
return Promise.resolve(layer);
});
Expand Down
8 changes: 1 addition & 7 deletions web/client/api/catalog/__tests__/COG-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
import { getLayerFromRecord, getCatalogRecords, validate } from '../COG';
import { COG_LAYER_TYPE } from '../../../utils/CatalogUtils';
import { getLayerFromRecord, getCatalogRecords, validate, getProjectionFromGeoKeys} from '../COG';
import expect from 'expect';


Expand Down Expand Up @@ -62,10 +62,4 @@ describe('COG (Abstraction) API', () => {
const service = {title: "some", records: [{url: "https://some.tif"}]};
expect(validate(service)).toBeTruthy();
});
it('test getProjectionFromGeoKeys', () => {
expect(getProjectionFromGeoKeys({geoKeys: {ProjectedCSTypeGeoKey: 4326}})).toBe('EPSG:4326');
expect(getProjectionFromGeoKeys({geoKeys: {GeographicTypeGeoKey: 3857}})).toBe('EPSG:3857');
expect(getProjectionFromGeoKeys({geoKeys: null})).toBe(null);
expect(getProjectionFromGeoKeys({geoKeys: {ProjectedCSTypeGeoKey: 32767}})).toBe(null);
});
});
9 changes: 7 additions & 2 deletions web/client/components/map/openlayers/plugins/COGLayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
*/

import Layers from '../../../../utils/openlayers/Layers';
import isEqual from 'lodash/isEqual';
import get from 'lodash/get';

import GeoTIFF from 'ol/source/GeoTIFF.js';
import TileLayer from 'ol/layer/WebGLTile.js';
Expand All @@ -15,7 +17,7 @@ import { isProjectionAvailable } from '../../../../utils/ProjectionUtils';
function create(options) {
return new TileLayer({
msId: options.id,
style: options.style, // TODO style needs to be improved. Currently renders only predefined band and ranges when specified in config
style: get(options, 'style.body'),
opacity: options.opacity !== undefined ? options.opacity : 1,
visible: options.visibility,
source: new GeoTIFF({
Expand All @@ -32,7 +34,10 @@ function create(options) {
Layers.registerType('cog', {
create,
update(layer, newOptions, oldOptions, map) {
if (newOptions.srs !== oldOptions.srs) {
if (newOptions.srs !== oldOptions.srs
|| !isEqual(newOptions.style, oldOptions.style)
|| !isEqual(newOptions.sources, oldOptions.sources) // min/max source data value can change
) {
return create(newOptions, map);
}
if (oldOptions.minResolution !== newOptions.minResolution) {
Expand Down
Loading
Loading