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

#9590: COG download metadata by default with abort fetch #9687

Merged
merged 4 commits into from
Nov 16, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 2 additions & 1 deletion build/buildConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,8 @@ module.exports = (...args) => mapArgumentsToObject(args, ({
jsonix: '@boundlessgeo/jsonix',
// next libs are added because of this issue https://github.com/geosolutions-it/MapStore2/issues/4569
proj4: '@geosolutions/proj4',
"react-joyride": '@geosolutions/react-joyride'
"react-joyride": '@geosolutions/react-joyride',
"geotiff-remote": path.join(paths.base, "node_modules", "geotiff", "dist-module", "source", "remote")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This solution is a big hack...

Did you try to implement it instead?
Like this as documented here, or better using axios (I'd prefer axios solution, so we have also authentication, proxy etc... out of the box, the doc do not show, for brevity, the solution with Promise) ?

const response = await fetch(someUrl);
const arrayBuffer = await response.arrayBuffer();
const tiff = await fromArrayBuffer(arrayBuffer);

If a solution like this is possible, we can have more control on it, without any hack.

Otherwise I think we have to rely on internal functions as you did in this implementation.

Copy link
Contributor Author

@dsuren1 dsuren1 Nov 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I did check.
fromArrayBuffer still doesn't handle signal parameter correctly. It is lost when the passed down to fromSource. Reason it cannot handled with axios is because fromSource makes bigtiff parsing logic to read tif data and converts to image object with required functions necessary for extracting metadata informations.

Maybe we can cancel the promise of fromUrl and getImage but not the actual fetch request action (which is a compromise) and in the process we can avoid the hack and also this will not impact user performance. Kindly let me know if this okay? @offtherailz

}, alias),
...(resolveModules && { modules: resolveModules })
},
Expand Down
3 changes: 2 additions & 1 deletion build/testConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,8 @@ module.exports = ({browsers = [ 'ChromeHeadless' ], files, path, testFile, singl
jsonix: '@boundlessgeo/jsonix',
// next libs are added because of this issue https://github.com/geosolutions-it/MapStore2/issues/4569
proj4: '@geosolutions/proj4',
"react-joyride": '@geosolutions/react-joyride'
"react-joyride": '@geosolutions/react-joyride',
"geotiff-remote": nodePath.join(__dirname, "..", "node_modules", "geotiff", "dist-module", "source", "remote")
}, alias),
extensions: ['.js', '.json', '.jsx']
},
Expand Down
7 changes: 7 additions & 0 deletions web/client/actions/__tests__/catalog-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,13 @@ describe('Test correctness of the catalog actions', () => {
expect(retval).toExist();
expect(retval.type).toBe(ADD_SERVICE);
});
it('addService with options', () => {
const options = {"test": "1"};
var retval = addService(options);
expect(retval).toExist();
expect(retval.type).toBe(ADD_SERVICE);
expect(retval.options).toEqual(options);
});
it('addCatalogService', () => {
var retval = addCatalogService(service);

Expand Down
5 changes: 3 additions & 2 deletions web/client/actions/catalog.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,9 +170,10 @@ export function changeUrl(url) {
url
};
}
export function addService() {
export function addService(options) {
return {
type: ADD_SERVICE
type: ADD_SERVICE,
options
};
}
export function addCatalogService(service) {
Expand Down
42 changes: 37 additions & 5 deletions web/client/api/catalog/COG.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@

import get from 'lodash/get';
import isEmpty from 'lodash/isEmpty';
import isNil from 'lodash/isNil';
import { Observable } from 'rxjs';
import { fromUrl } from 'geotiff';
import { GeoTIFF } from 'geotiff';
import { makeRemoteSource } from 'geotiff-remote';

import { isValidURL } from '../../utils/URLUtils';
import ConfigUtils from '../../utils/ConfigUtils';
Expand Down Expand Up @@ -57,8 +59,22 @@ export const getProjectionFromGeoKeys = (image) => {

return null;
};
const abortError = (reject) => reject(new DOMException("Aborted", "AbortError"));
/**
* getImage with abort fetching of data slices
*/
const getImage = (geotiff, signal) => {
if (signal?.aborted) {
return abortError(Promise.reject);
}
return new Promise((resolve, reject) => {
signal?.addEventListener("abort", () => abortError(reject));
return geotiff.getImage() // Fetch and read first image to get medatadata of the tif
.then((image) => resolve(image))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically, as discussed with @dsuren1 , we can canel the pending task to make the user continue operating if the "Save" takes too long, but we can not cancel the effective requests pending

This can cause the pending requests to be blocking and slowing down the load on that particular domain in any case (because of bandwitdh and max number of parallel requests allowed to a specific domain by the browser etc...)

Please report the known issue notice, or we loose memory of it.
Having an issue about it linking could be useful to track the problem too.
@tdipisa should we open it ?

I ask @dsuren1 to link the proper issue to github repo of geotiffjs, for completeness.

Suggested change
.then((image) => resolve(image))
.then((image) => resolve(image)) // note: the cacellation will not cancel the request because of issue in

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated note on the function and created an issue in geotiff.js

.catch(()=> abortError(reject));
});
};
let capabilitiesCache = {};

export const getRecords = (_url, startPosition, maxRecords, text, info = {}) => {
const service = get(info, 'options.service');
let layers = [];
Expand All @@ -73,19 +89,35 @@ export const getRecords = (_url, startPosition, maxRecords, text, info = {}) =>
sources: [{url}],
options: service.options || {}
};
if (service.fetchMetadata) {
const controller = get(info, 'options.controller');
const isSave = get(info, 'options.save', false);
// 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)
.then(geotiff => geotiff.getImage())
const signal = controller?.signal;
// geotiff's `fromUrl` & `getImage` function doesn't pass down signal parameter properly. Known issue (https://github.com/geotiffjs/geotiff.js/issues/330)
// Hence `fromSource` is called directly with source formulated
return GeoTIFF.fromSource(makeRemoteSource(url, {}), {}, signal)
.then(geotiff => getImage(geotiff, 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) && isProjectionDefined && {
bbox: {
Expand Down
28 changes: 22 additions & 6 deletions web/client/components/catalog/CatalogServiceEditor.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,20 @@ import Message from "../I18N/Message";
import AdvancedSettings from './editor/AdvancedSettings';
import MainForm from './editor/MainForm';

export default ({
const withAbort = (Component) => {
return (props) => {
const [abortController, setAbortController] = useState(null);
dsuren1 marked this conversation as resolved.
Show resolved Hide resolved
const onSave = () => {
// Currently abort request on saving is applicable only for COG service
const controller = props.format === 'cog' ? new AbortController() : null;
setAbortController(controller);
return props.onAddService({save: true, controller});
};
const onCancel = () => abortController && props.saving ? abortController?.abort() : props.onChangeCatalogMode("view");
return <Component {...props} onCancel={onCancel} onSaveService={onSave} disabled={!abortController && props.saving} />;
};
};
const CatalogServiceEditor = ({
service = {
title: "",
type: "wms",
Expand All @@ -39,9 +52,9 @@ export default ({
onChangeServiceProperty = () => {},
onToggleTemplate = () => {},
onToggleThumbnail = () => {},
onAddService = () => {},
onDeleteService = () => {},
onChangeCatalogMode = () => {},
onCancel = () => {},
onSaveService = () => {},
onFormatOptionsFetch = () => {},
selectedService,
isLocalizedLayerStylesEnabled,
Expand All @@ -50,7 +63,8 @@ export default ({
layerOptions = {},
infoFormatOptions,
services,
autoSetVisibilityLimits = false
autoSetVisibilityLimits = false,
disabled
}) => {
const [valid, setValid] = useState(true);
return (<BorderLayout
Expand Down Expand Up @@ -92,7 +106,7 @@ export default ({
/>
<FormGroup controlId="buttons" key="buttons">
<Col xs={12}>
<Button style={buttonStyle} disabled={saving || !valid} onClick={() => onAddService()} key="catalog_add_service_button">
<Button style={buttonStyle} disabled={saving || !valid} onClick={onSaveService} key="catalog_add_service_button">
{saving ? <Loader size={12} style={{display: 'inline-block'}} /> : null}
<Message msgId="save" />
</Button>
Expand All @@ -102,11 +116,13 @@ export default ({
</Button>
: null
}
<Button style={buttonStyle} disabled={saving} onClick={() => onChangeCatalogMode("view")} key="catalog_back_view_button">
<Button style={buttonStyle} disabled={disabled} onClick={onCancel} key="catalog_back_view_button">
<Message msgId="cancel" />
</Button>
</Col>
</FormGroup>
</Form>
</BorderLayout>);
};

export default withAbort(CatalogServiceEditor);
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import React from 'react';
import ReactDOM from 'react-dom';
import expect from 'expect';
import TestUtils from 'react-dom/test-utils';
import CatalogServiceEditor from '../CatalogServiceEditor';
import {defaultPlaceholder} from "../editor/MainFormUtils";

Expand Down Expand Up @@ -149,4 +150,78 @@ describe('Test CatalogServiceEditor', () => {
let placeholder = defaultPlaceholder(service);
expect(placeholder).toBe("e.g. https://mydomain.com/geoserver/wms");
});
it('test saving service for COG type', () => {
const actions = {
onAddService: () => {}
};
const spyOnAdd = expect.spyOn(actions, 'onAddService');
ReactDOM.render(<CatalogServiceEditor
format="cog"
onAddService={actions.onAddService}
/>, document.getElementById("container"));
let buttons = document.querySelectorAll('.form-group button');
let saveBtn;
buttons.forEach(btn => {if (btn.textContent === 'save') saveBtn = btn;});
expect(saveBtn).toBeTruthy();
TestUtils.Simulate.click(saveBtn);
expect(spyOnAdd).toHaveBeenCalled();
let arg = spyOnAdd.calls[0].arguments[0];
expect(arg.save).toBe(true);
expect(arg.controller).toBeTruthy();

ReactDOM.render(<CatalogServiceEditor
format="csw"
onAddService={actions.onAddService}
/>, document.getElementById("container"));
buttons = document.querySelectorAll('.form-group button');
buttons.forEach(btn => {if (btn.textContent === 'save') saveBtn = btn;});
expect(saveBtn).toBeTruthy();
TestUtils.Simulate.click(saveBtn);
expect(spyOnAdd).toHaveBeenCalled();
arg = spyOnAdd.calls[1].arguments[0];
expect(arg.save).toBeTruthy();
expect(arg.controller).toBeFalsy();
});
it('test cancel service', () => {
const actions = {
onChangeCatalogMode: () => {},
onAddService: () => {}
};
const spyOnCancel = expect.spyOn(actions, 'onChangeCatalogMode');
ReactDOM.render(<CatalogServiceEditor
format="csw"
onChangeCatalogMode={actions.onChangeCatalogMode}
/>, document.getElementById("container"));
let buttons = document.querySelectorAll('.form-group button');
let cancelBtn;
buttons.forEach(btn => {if (btn.textContent === 'cancel') cancelBtn = btn;});
expect(cancelBtn).toBeTruthy();
TestUtils.Simulate.click(cancelBtn);
expect(spyOnCancel).toHaveBeenCalled();
let arg = spyOnCancel.calls[0].arguments[0];
expect(arg).toBe('view');

const spyOnAdd = expect.spyOn(actions, 'onAddService');
ReactDOM.render(<CatalogServiceEditor
format="cog"
onChangeCatalogMode={actions.onChangeCatalogMode}
onAddService={actions.onAddService}
/>, document.getElementById("container"));
buttons = document.querySelectorAll('.form-group button');
let saveBtn;
buttons.forEach(btn => {if (btn.textContent === 'save') saveBtn = btn;});
TestUtils.Simulate.click(saveBtn);
expect(spyOnAdd).toHaveBeenCalled();

ReactDOM.render(<CatalogServiceEditor
format="cog"
saving
onChangeCatalogMode={actions.onChangeCatalogMode}
onAddService={actions.onAddService}
/>, document.getElementById("container"));
buttons = document.querySelectorAll('.form-group button');
buttons.forEach(btn => {if (btn.textContent === 'cancel') cancelBtn = btn;});
TestUtils.Simulate.click(cancelBtn);
expect(spyOnCancel.calls[1]).toBeFalsy();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export default ({
<Col xs={12}>
<Checkbox
onChange={(e) => onChangeServiceProperty("fetchMetadata", e.target.checked)}
checked={!isNil(service.fetchMetadata) ? service.fetchMetadata : false}>
checked={!isNil(service.fetchMetadata) ? service.fetchMetadata : true}>
<Message msgId="catalog.fetchMetadata.label" />&nbsp;<InfoPopover text={<Message msgId="catalog.fetchMetadata.tooltip" />} />
</Checkbox>
</Col>
Expand Down
4 changes: 2 additions & 2 deletions web/client/epics/catalog.js
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ export default (API) => ({
*/
newCatalogServiceAdded: (action$, store) =>
action$.ofType(ADD_SERVICE)
.switchMap(() => {
.switchMap(({options} = {}) => {
const state = store.getState();
const newService = newServiceSelector(state);
const maxRecords = pageSizeSelector(state);
Expand All @@ -310,7 +310,7 @@ export default (API) => ({
startPosition: 1,
maxRecords,
text: "",
options: {service, isNewService: true}
options: {service, isNewService: true, ...options}
})
);
})
Expand Down
2 changes: 1 addition & 1 deletion web/client/translations/data.de-DE.json
Original file line number Diff line number Diff line change
Expand Up @@ -1553,7 +1553,7 @@
"tooltip": "Fügen Sie der Karte Ebenen hinzu",
"autoload": "Suche in Dienstauswahl",
"fetchMetadata": {
"label": "Laden Sie Dateimetadaten bei der Suche herunter",
"label": "Dateimetadaten beim Speichern herunterladen",
"tooltip": "Diese Option ruft Metadaten ab, um das Zoomen auf Ebene zu unterstützen. Es kann den Suchvorgang verlangsamen, wenn die Bilder zu groß oder zu viele sind."
},
"clearValueText": "Auswahl aufheben",
Expand Down
2 changes: 1 addition & 1 deletion web/client/translations/data.en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -1514,7 +1514,7 @@
"tooltip": "Add layers to the map",
"autoload": "Search on service selection",
"fetchMetadata": {
"label": "Download file metadata on search",
"label": "Download file metadata on save",
"tooltip": "This option will fetch metadata to support the zoom to layer. It may slow down the search operation if the images are too big or too many."
},
"clearValueText": "Clear selection",
Expand Down
2 changes: 1 addition & 1 deletion web/client/translations/data.es-ES.json
Original file line number Diff line number Diff line change
Expand Up @@ -1515,7 +1515,7 @@
"tooltip": "agregar capas al mapa",
"autoload": "Buscar en la selección de servicios",
"fetchMetadata": {
"label": "Descargar metadatos de archivos en la búsqueda",
"label": "Descargar metadatos del archivo al guardar",
"tooltip": "Esta opción recuperará metadatos para admitir el zoom a la capa. Puede ralentizar la operación de búsqueda si las imágenes son demasiado grandes o demasiadas."
},
"clearValueText": "Borrar selección",
Expand Down
2 changes: 1 addition & 1 deletion web/client/translations/data.fr-FR.json
Original file line number Diff line number Diff line change
Expand Up @@ -1515,7 +1515,7 @@
"tooltip": "Ajouter des couches à la carte",
"autoload": "Recherche sur la sélection du service",
"fetchMetadata": {
"label": "Télécharger les métadonnées du fichier lors de la recherche",
"label": "Télécharger les métadonnées du fichier lors de l'enregistrement",
"tooltip": "Cette option récupérera les métadonnées pour prendre en charge le zoom sur la couche. Cela peut ralentir l'opération de recherche si les images sont trop grandes ou trop nombreuses."
},
"clearValueText": "Effacer la sélection",
Expand Down
2 changes: 1 addition & 1 deletion web/client/translations/data.it-IT.json
Original file line number Diff line number Diff line change
Expand Up @@ -1513,7 +1513,7 @@
"title": "Catalogo",
"autoload": "Ricerca alla selezione del servizio",
"fetchMetadata": {
"label": "Scarica i metadati dei file durante la ricerca",
"label": "Scarica i metadati del file al salvataggio",
"tooltip": "Questa opzione recupererà i metadati per supportare lo zoom a livello. Potrebbe rallentare l'operazione di ricerca se le immagini sono troppo grandi o troppe."
},
"clearValueText": "Cancella selezione",
Expand Down