Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
Binary file added examples/geotiff-example.tif
Binary file not shown.
17 changes: 13 additions & 4 deletions packages/base/src/dialogs/symbology/hooks/useGetBandInfo.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { IDict, IJGISLayer, IJupyterGISModel } from '@jupytergis/schema';
import { DocumentRegistry } from '@jupyterlab/docregistry';
import { useEffect, useState } from 'react';
import { loadGeoTIFFWithCache } from '../../../tools';
import { loadFile } from '../../../tools';

export interface IBandHistogram {
buckets: number[];
Expand Down Expand Up @@ -34,8 +34,17 @@ interface ITifBandData {
histogram: any;
}

const preloadGeoTiffFile = async (sourceInfo: { url?: string | undefined }) => {
return await loadGeoTIFFWithCache(sourceInfo);
const preloadGeoTiffFile = async (
sourceInfo: {
url?: string | undefined;
},
model: IJupyterGISModel
): Promise<{ file: Blob; metadata: any; sourceUrl: string }> => {
return await loadFile({
filepath: sourceInfo.url ?? '',
type: 'GeoTiffSource',
model: model
});
};

const useGetBandInfo = (
Expand All @@ -61,7 +70,7 @@ const useGetBandInfo = (
return;
}

const preloadedFile = await preloadGeoTiffFile(sourceInfo);
const preloadedFile = await preloadGeoTiffFile(sourceInfo, context.model);
const { file, metadata, sourceUrl } = { ...preloadedFile };

if (file && metadata && sourceUrl === sourceInfo.url) {
Expand Down
24 changes: 19 additions & 5 deletions packages/base/src/formbuilder/objectform/fileselectorwidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export const FileSelectorWidget = (props: any) => {
}

const output = await FileDialog.getOpenFiles({
title: 'Select a File',
title: `Select ${formOptions.sourceType.split('Source')[0]} File`,
manager: docManager
});

Expand All @@ -62,10 +62,24 @@ export const FileSelectorWidget = (props: any) => {
props.onChange(relativePath);

if (dialogElement) {
formOptions.dialogOptions.sourceData = {
...formOptions.sourceData,
path: relativePath
};
if (formOptions.sourceType === 'GeoTiffSource') {
formOptions.dialogOptions.sourceData = {
...formOptions.sourceData,
urls: formOptions.dialogOptions.sourceData.urls.map(
(urlObject: any) => {
return {
...urlObject,
url: relativePath
};
}
)
};
} else {
formOptions.dialogOptions.sourceData = {
...formOptions.sourceData,
path: relativePath
};
}

const formDialog = new CreationFormDialog({
...formOptions.dialogOptions
Expand Down
123 changes: 98 additions & 25 deletions packages/base/src/formbuilder/objectform/geotiffsource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,80 @@ import { showErrorMessage } from '@jupyterlab/apputils';
import { IChangeEvent, ISubmitEvent } from '@rjsf/core';

import { BaseForm, IBaseFormProps } from './baseform';
import { FileSelectorWidget } from './fileselectorwidget';
import { loadFile } from '../../tools';

/**
* The form to modify a GeoTiff source.
*/
export class GeoTiffSourcePropertiesForm extends BaseForm {
private _isSubmitted: boolean;

constructor(props: IBaseFormProps) {
super(props);

this._isSubmitted = false;
this._validateUrls(props.sourceData?.urls ?? []);
}

protected processSchema(
data: IDict<any> | undefined,
schema: IDict,
uiSchema: IDict
) {
super.processSchema(data, schema, uiSchema);
if (!schema.properties || !data) {
return;
}

// Customize the widget for urls
if (schema.properties && schema.properties.urls) {
const docManager =
this.props.formChangedSignal?.sender.props.formSchemaRegistry.getDocManager();

uiSchema.urls = {
...uiSchema.urls,
items: {
...uiSchema.urls.items,
url: {
'ui:widget': FileSelectorWidget,
'ui:options': {
docManager,
formOptions: this.props
}
}
}
};
}

// This is not user-editable
delete schema.properties.valid;
}

protected onFormChange(e: IChangeEvent): void {
super.onFormChange(e);
if (e.formData?.urls) {
this._validateUrls(e.formData.urls);
}
}

protected onFormSubmit(e: ISubmitEvent<any>) {
if (this.state.extraErrors?.urls?.__errors?.length >= 1) {
showErrorMessage('Invalid URLs', this.state.extraErrors.urls.__errors[0]);
protected onFormBlur(id: string, value: any) {
// Is there a better way to spot the url text entry?
if (!id.endsWith('_urls')) {
return;
}
this._validateUrls(value);
}

protected async onFormSubmit(e: ISubmitEvent<any>) {
this._isSubmitted = true;

// validate urls.url only when submitting for better performance
const { valid, errors } = await this._validateUrls(e.formData.urls);
if (!valid) {
if (errors.length > 0) {
showErrorMessage('Invalid URLs', errors[0]);
}
return;
}
super.onFormSubmit(e);
Expand All @@ -41,31 +95,49 @@ export class GeoTiffSourcePropertiesForm extends BaseForm {
if (urls && urls.length > 0) {
for (let i = 0; i < urls.length; i++) {
const { url, min, max } = urls[i];
if (this._isSubmitted) {
try {
await loadFile({
filepath: url,
type: this.props.sourceType,
model: this.props.model
});
} catch (e) {
valid = false;
errors.push(
`"${url}" is not a valid ${this.props.sourceType} file: ${e}.`
);
}
} else {
if (!url || typeof url !== 'string' || url.trim() === '') {
valid = false;
errors.push(
`URL at index ${i} is required and must be a valid string.`
);
}

if (!url || typeof url !== 'string' || url.trim() === '') {
errors.push(
`URL at index ${i} is required and must be a valid string.`
);
valid = false;
}
if (min === undefined || typeof min !== 'number') {
errors.push(
`Min value at index ${i} is required and must be a number.`
);
valid = false;
}

if (min === undefined || typeof min !== 'number') {
errors.push(
`Min value at index ${i} is required and must be a number.`
);
valid = false;
}

if (max === undefined || typeof max !== 'number') {
errors.push(
`Max value at index ${i} is required and must be a number.`
);
valid = false;
}
if (max === undefined || typeof max !== 'number') {
errors.push(
`Max value at index ${i} is required and must be a number.`
);
valid = false;
}

if (typeof min === 'number' && typeof max === 'number' && max <= min) {
errors.push(`Max value at index ${i} must be greater than Min.`);
valid = false;
if (
typeof min === 'number' &&
typeof max === 'number' &&
max <= min
) {
errors.push(`Max value at index ${i} must be greater than Min.`);
valid = false;
}
}
}
} else {
Expand All @@ -88,5 +160,6 @@ export class GeoTiffSourcePropertiesForm extends BaseForm {
if (this.props.formErrorSignal) {
this.props.formErrorSignal.emit(!valid);
}
return { valid, errors };
}
}
26 changes: 11 additions & 15 deletions packages/base/src/mainview/mainView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,7 @@ import * as React from 'react';
import AnnotationFloater from '../annotations/components/AnnotationFloater';
import { CommandIDs } from '../constants';
import StatusBar from '../statusbar/StatusBar';
import {
isLightTheme,
loadFile,
loadGeoTIFFWithCache,
throttle
} from '../tools';
import { isLightTheme, loadFile, throttle } from '../tools';
import CollaboratorPointers, { ClientPointer } from './CollaboratorPointers';
import { FollowIndicator } from './FollowIndicator';
import { MainViewModel } from './mainviewmodel';
Expand Down Expand Up @@ -451,13 +446,6 @@ export class MainView extends React.Component<IProps, IStates> {
});
};

private async _loadGeoTIFFWithCache(sourceInfo: {
url?: string | undefined;
}) {
const result = await loadGeoTIFFWithCache(sourceInfo);
return result?.file;
}

/**
* Add a source in the map.
*
Expand Down Expand Up @@ -639,8 +627,16 @@ export class MainView extends React.Component<IProps, IStates> {
};
const sourcesWithBlobs = await Promise.all(
sourceParameters.urls.map(async sourceInfo => {
const blob = await this._loadGeoTIFFWithCache(sourceInfo);
return { ...addNoData(sourceInfo), blob };
const geotiff = await loadFile({
filepath: sourceInfo.url ?? '',
type: 'GeoTiffSource',
model: this._model
});
return {
...addNoData(sourceInfo),
geotiff,
url: URL.createObjectURL(geotiff.file)
};
})
);

Expand Down
54 changes: 46 additions & 8 deletions packages/base/src/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Protobuf from 'pbf';
import { VectorTile } from '@mapbox/vector-tile';

import { URLExt } from '@jupyterlab/coreutils';
import { ServerConnection } from '@jupyterlab/services';
import { Contents, ServerConnection } from '@jupyterlab/services';
import * as d3Color from 'd3-color';
import { PathExt } from '@jupyterlab/coreutils';
import shp from 'shpjs';
Expand Down Expand Up @@ -428,13 +428,21 @@ export const getFromIndexedDB = async (key: string) => {
* @param sourceInfo object containing the URL of the GeoTIFF file.
* @returns A promise that resolves to the file as a Blob, or undefined .
*/
export const loadGeoTIFFWithCache = async (sourceInfo: {
url?: string | undefined;
}) => {
export const loadGeoTiff = async (
sourceInfo: {
url?: string | undefined;
},
file?: Contents.IModel | null
) => {
if (!sourceInfo?.url) {
return null;
}

const mimeType = getMimeType(sourceInfo.url);
if (!mimeType || !mimeType.startsWith('image/tiff')) {
throw new Error('Invalid file type. Expected GeoTIFF (image/tiff).');
}

const cachedData = await getFromIndexedDB(sourceInfo.url);
if (cachedData) {
return {
Expand All @@ -444,12 +452,23 @@ export const loadGeoTIFFWithCache = async (sourceInfo: {
};
}

const response = await fetch(sourceInfo.url);
const fileBlob = await response.blob();
const file = new File([fileBlob], 'loaded.tif');
let fileBlob: Blob;
if (!file) {
const response = await fetch(
`/jupytergis_core/proxy?url=${sourceInfo.url}`
);
if (!response.ok) {
throw new Error(`Failed to fetch file. Status: ${response.status}`);
}
fileBlob = await response.blob();
} else {
fileBlob = await base64ToBlob(file.content, mimeType);
}

const geotiff = new File([fileBlob], 'loaded.tif');

const Gdal = await getGdal();
const result = await Gdal.open(file);
const result = await Gdal.open(geotiff);
const tifDataset = result.datasets[0];
const metadata = await Gdal.gdalinfo(tifDataset, ['-stats']);
Gdal.close(tifDataset);
Expand Down Expand Up @@ -541,6 +560,16 @@ export const loadFile = async (fileInfo: {
}
}

case 'GeoTiffSource': {
try {
const tiff = loadGeoTiff({ url: filepath });
return tiff;
} catch (error) {
console.error('Error loading remote GeoTIFF:', error);
throw error;
}
}

default: {
throw new Error(`Unsupported URL handling for source type: ${type}`);
}
Expand Down Expand Up @@ -598,6 +627,15 @@ export const loadFile = async (fileInfo: {
}
}

case 'GeoTiffSource': {
if (typeof file.content === 'string') {
const tiff = loadGeoTiff({ url: filepath }, file);
return tiff;
} else {
throw new Error('Invalid file format for tiff content.');
}
}

default: {
throw new Error(`Unsupported source type: ${type}`);
}
Expand Down
Loading