Skip to content

Commit accdf21

Browse files
authored
Restore local geotiff file support (#571)
* Get back local geotiff file support * cleanup * lint * fix import * handle symbology panel for local file too * lint
1 parent 7aae34d commit accdf21

File tree

4 files changed

+140
-10
lines changed

4 files changed

+140
-10
lines changed

packages/base/src/dialogs/symbology/hooks/useGetBandInfo.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { IJGISLayer, IJupyterGISModel } from '@jupytergis/schema';
22
import { useEffect, useState } from 'react';
3-
import { fromUrl } from 'geotiff';
3+
import { fromUrl, fromBlob } from 'geotiff';
4+
import { loadFile } from '../../../tools';
45

56
export interface IBandHistogram {
67
buckets: number[];
@@ -38,8 +39,30 @@ const useGetBandInfo = (model: IJupyterGISModel, layer: IJGISLayer) => {
3839
return;
3940
}
4041

41-
// TODO Get band names + get band stats
42-
const tiff = await fromUrl(sourceInfo.url);
42+
let tiff;
43+
if (
44+
sourceInfo.url.startsWith('http') ||
45+
sourceInfo.url.startsWith('https')
46+
) {
47+
// Handle remote GeoTIFF file
48+
tiff = await fromUrl(sourceInfo.url);
49+
} else {
50+
// Handle local GeoTIFF file
51+
const preloadedFile = await loadFile({
52+
filepath: sourceInfo.url,
53+
type: 'GeoTiffSource',
54+
model
55+
});
56+
57+
if (!preloadedFile.file) {
58+
setError('Failed to load local file.');
59+
setLoading(false);
60+
return;
61+
}
62+
63+
tiff = await fromBlob(preloadedFile.file);
64+
}
65+
4366
const image = await tiff.getImage();
4467
const numberOfBands = image.getSamplesPerPixel();
4568

packages/base/src/formbuilder/objectform/source/geotiffsource.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { IChangeEvent, ISubmitEvent } from '@rjsf/core';
44

55
import { getMimeType } from '../../../tools';
66
import { ISourceFormProps, SourcePropertiesForm } from './sourceform';
7+
import { FileSelectorWidget } from '../fileselectorwidget';
78

89
/**
910
* The form to modify a GeoTiff source.
@@ -28,6 +29,26 @@ export class GeoTiffSourcePropertiesForm extends SourcePropertiesForm {
2829
return;
2930
}
3031

32+
// Customize the widget for urls
33+
if (schema.properties && schema.properties.urls) {
34+
const docManager =
35+
this.props.formChangedSignal?.sender.props.formSchemaRegistry.getDocManager();
36+
37+
uiSchema.urls = {
38+
...uiSchema.urls,
39+
items: {
40+
...uiSchema.urls.items,
41+
url: {
42+
'ui:widget': FileSelectorWidget,
43+
'ui:options': {
44+
docManager,
45+
formOptions: this.props
46+
}
47+
}
48+
}
49+
};
50+
}
51+
3152
// This is not user-editable
3253
delete schema.properties.valid;
3354
}

packages/base/src/mainview/mainView.tsx

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -634,12 +634,31 @@ export class MainView extends React.Component<IProps, IStates> {
634634
};
635635
const sources = await Promise.all(
636636
sourceParameters.urls.map(async sourceInfo => {
637-
return {
638-
...addNoData(sourceInfo),
639-
min: sourceInfo.min,
640-
max: sourceInfo.max,
641-
url: sourceInfo.url
642-
};
637+
const isRemote =
638+
sourceInfo.url?.startsWith('http://') ||
639+
sourceInfo.url?.startsWith('https://');
640+
641+
if (isRemote) {
642+
return {
643+
...addNoData(sourceInfo),
644+
min: sourceInfo.min,
645+
max: sourceInfo.max,
646+
url: sourceInfo.url
647+
};
648+
} else {
649+
const geotiff = await loadFile({
650+
filepath: sourceInfo.url ?? '',
651+
type: 'GeoTiffSource',
652+
model: this._model
653+
});
654+
return {
655+
...addNoData(sourceInfo),
656+
min: sourceInfo.min,
657+
max: sourceInfo.max,
658+
geotiff,
659+
url: URL.createObjectURL(geotiff.file)
660+
};
661+
}
643662
})
644663
);
645664

packages/base/src/tools.ts

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ import Protobuf from 'pbf';
33
import { VectorTile } from '@mapbox/vector-tile';
44

55
import { PathExt, URLExt } from '@jupyterlab/coreutils';
6-
import { ServerConnection } from '@jupyterlab/services';
6+
import { Contents, ServerConnection } from '@jupyterlab/services';
77
import { showErrorMessage } from '@jupyterlab/apputils';
88
import * as d3Color from 'd3-color';
99
import shp from 'shpjs';
10+
import { getGdal } from './gdal';
1011

1112
import {
1213
IDict,
@@ -435,6 +436,63 @@ const fetchWithProxies = async <T>(
435436
return null;
436437
};
437438

439+
/**
440+
* Load a GeoTIFF file from IndexedDB database cache or fetch it .
441+
*
442+
* @param sourceInfo object containing the URL of the GeoTIFF file.
443+
* @returns A promise that resolves to the file as a Blob, or undefined .
444+
*/
445+
export const loadGeoTiff = async (
446+
sourceInfo: { url?: string | undefined },
447+
file?: Contents.IModel | null
448+
) => {
449+
if (!sourceInfo?.url) {
450+
return null;
451+
}
452+
453+
const url = sourceInfo.url;
454+
const mimeType = getMimeType(url);
455+
if (!mimeType || !mimeType.startsWith('image/tiff')) {
456+
throw new Error('Invalid file type. Expected GeoTIFF (image/tiff).');
457+
}
458+
459+
const cachedData = await getFromIndexedDB(url);
460+
if (cachedData) {
461+
return {
462+
file: cachedData.file,
463+
metadata: cachedData.metadata,
464+
sourceUrl: url
465+
};
466+
}
467+
468+
let fileBlob: Blob | null = null;
469+
470+
if (!file) {
471+
fileBlob = await fetchWithProxies(url, async response => response.blob());
472+
if (!fileBlob) {
473+
showErrorMessage('Network error', `Failed to fetch ${url}`);
474+
throw new Error(`Failed to fetch ${url}`);
475+
}
476+
} else {
477+
fileBlob = await base64ToBlob(file.content, mimeType);
478+
}
479+
480+
const geotiff = new File([fileBlob], 'loaded.tif');
481+
const Gdal = await getGdal();
482+
const result = await Gdal.open(geotiff);
483+
const tifDataset = result.datasets[0];
484+
const metadata = await Gdal.gdalinfo(tifDataset, ['-stats']);
485+
Gdal.close(tifDataset);
486+
487+
await saveToIndexedDB(url, fileBlob, metadata);
488+
489+
return {
490+
file: fileBlob,
491+
metadata,
492+
sourceUrl: url
493+
};
494+
};
495+
438496
/**
439497
* Generalized file reader for different source types.
440498
*
@@ -567,6 +625,15 @@ export const loadFile = async (fileInfo: {
567625
}
568626
}
569627

628+
case 'GeoTiffSource': {
629+
if (typeof file.content === 'string') {
630+
const tiff = loadGeoTiff({ url: filepath }, file);
631+
return tiff;
632+
} else {
633+
throw new Error('Invalid file format for tiff content.');
634+
}
635+
}
636+
570637
default: {
571638
throw new Error(`Unsupported source type: ${type}`);
572639
}

0 commit comments

Comments
 (0)