From e787dfd6e9757957c2e1801c5b7d0302a67cb7bd Mon Sep 17 00:00:00 2001 From: arjxn-py Date: Fri, 21 Mar 2025 18:26:28 +0530 Subject: [PATCH 1/3] Processing: Rasterize a vector layer --- packages/base/src/commands.ts | 105 ++++++++++++++++++++++++++++- packages/base/src/constants.ts | 1 + packages/base/src/tools.ts | 8 +++ python/jupytergis_lab/src/index.ts | 4 ++ 4 files changed, 117 insertions(+), 1 deletion(-) diff --git a/packages/base/src/commands.ts b/packages/base/src/commands.ts index cee8fa05d..2e05fe1c5 100644 --- a/packages/base/src/commands.ts +++ b/packages/base/src/commands.ts @@ -24,7 +24,7 @@ import keybindings from './keybindings.json'; import { JupyterGISTracker } from './types'; import { JupyterGISDocumentWidget } from './widget'; import { getGdal } from './gdal'; -import { getGeoJSONDataFromLayerSource, downloadFile } from './tools'; +import { getGeoJSONDataFromLayerSource, downloadFile, uint8ArrayToBase64 } from './tools'; import { IJGISLayer, IJGISSource } from '@jupytergis/schema'; import { UUID } from '@lumino/coreutils'; import { ProcessingFormDialog } from './dialogs/ProcessingFormDialog'; @@ -1400,6 +1400,109 @@ export function addCommands( } }); + commands.addCommand(CommandIDs.rasterize, { + label: trans.__('Rasterize'), + isEnabled: () => { + const selectedLayer = getSingleSelectedLayer(tracker); + return selectedLayer + ? ['VectorLayer', 'ShapefileLayer'].includes(selectedLayer.type) + : false; + }, + execute: async () => { + const selectedLayer = getSingleSelectedLayer(tracker); + if (!selectedLayer) { + return; + } + + const model = tracker.currentWidget?.model as IJupyterGISModel; + const sources = model.sharedModel.sources ?? {}; + + const exportSchema = { + ...(formSchemaRegistry.getSchemas().get('ExportGeoTIFFSchema') as IDict) + }; + console.log(exportSchema); + + const formValues = await new Promise(resolve => { + const dialog = new ProcessingFormDialog({ + title: 'Download GeoTIFF', + schema: exportSchema, + model, + formContext: 'create', + processingType: 'export', + sourceData: { resolutionX: 1200, resolutionY: 1200 }, + syncData: (props: IDict) => { + resolve(props); + dialog.dispose(); + } + }); + + dialog.launch(); + }); + + if (!formValues || !selectedLayer.parameters) { + return; + } + + const exportFileName = formValues.exportFileName; + const resolutionX = formValues.resolutionX ?? 1200; + const resolutionY = formValues.resolutionY ?? 1200; + const sourceId = selectedLayer.parameters.source; + const source = sources[sourceId]; + + const geojsonString = await getGeoJSONDataFromLayerSource(source, model); + if (!geojsonString) { + return; + } + + const Gdal = await getGdal(); + const datasetList = await Gdal.open( + new File([geojsonString], 'data.geojson', { + type: 'application/geo+json' + }) + ); + const dataset = datasetList.datasets[0]; + + if (!dataset) { + console.error('Dataset could not be opened.'); + return; + } + + const options = [ + '-of', + 'GTiff', + '-ot', + 'Float32', + '-a_nodata', + '-1.0', + '-burn', + '0.0', + '-ts', + resolutionX.toString(), + resolutionY.toString(), + '-l', + 'data', + 'data.geojson', + 'output.tif' + ]; + + const outputFilePath = await Gdal.gdal_rasterize(dataset, options); + const exportedBytes = await Gdal.getFileBytes(outputFilePath); + + const base64String = uint8ArrayToBase64(exportedBytes); + + + const savePath = `examples/${exportFileName}.tif`; + + await app.serviceManager.contents.save(savePath, { + type: 'file', + format: 'base64', + content: base64String + }); + + Gdal.close(dataset); + } + }); + loadKeybindings(commands, keybindings); } diff --git a/packages/base/src/constants.ts b/packages/base/src/constants.ts index eb91c975d..13201ffe0 100644 --- a/packages/base/src/constants.ts +++ b/packages/base/src/constants.ts @@ -27,6 +27,7 @@ export namespace CommandIDs { // Processing commands export const buffer = 'jupytergis:buffer'; export const dissolve = 'jupytergis:dissolve'; + export const rasterize = 'jupytergis:rasterize'; // Sources only commands export const newRasterSource = 'jupytergis:newRasterSource'; diff --git a/packages/base/src/tools.ts b/packages/base/src/tools.ts index 1139959d9..253edf5df 100644 --- a/packages/base/src/tools.ts +++ b/packages/base/src/tools.ts @@ -855,3 +855,11 @@ export async function getGeoJSONDataFromLayerSource( console.error("Source is missing both 'path' and 'data' parameters."); return null; } + +export const uint8ArrayToBase64 = (uint8Array: Uint8Array): string => { + let binaryString = ''; + for (let i = 0; i < uint8Array.length; i++) { + binaryString += String.fromCharCode(uint8Array[i]); + } + return btoa(binaryString); +}; diff --git a/python/jupytergis_lab/src/index.ts b/python/jupytergis_lab/src/index.ts index 4178bdbd3..e53021890 100644 --- a/python/jupytergis_lab/src/index.ts +++ b/python/jupytergis_lab/src/index.ts @@ -201,6 +201,10 @@ const plugin: JupyterFrontEndPlugin = { command: CommandIDs.dissolve }); + processingSubmenu.addItem({ + command: CommandIDs.rasterize + }); + app.contextMenu.addItem({ type: 'submenu', selector: '.jp-gis-layerItem', From 79b2cfabe807b1ed89d02327393421d58a75870b Mon Sep 17 00:00:00 2001 From: arjxn-py Date: Fri, 28 Mar 2025 17:30:47 +0530 Subject: [PATCH 2/3] add rasterized geotiff to model --- packages/base/src/commands.ts | 30 +++++++++++++++---- .../base/src/dialogs/ProcessingFormDialog.tsx | 2 -- .../src/schema/export/exportGeotiff.json | 10 ++----- 3 files changed, 26 insertions(+), 16 deletions(-) diff --git a/packages/base/src/commands.ts b/packages/base/src/commands.ts index a2e73cdd5..7c35370d8 100644 --- a/packages/base/src/commands.ts +++ b/packages/base/src/commands.ts @@ -26,7 +26,7 @@ import keybindings from './keybindings.json'; import { JupyterGISTracker } from './types'; import { JupyterGISDocumentWidget } from './widget'; import { getGdal } from './gdal'; -import { getGeoJSONDataFromLayerSource, downloadFile } from './tools'; +import { getGeoJSONDataFromLayerSource, downloadFile, uint8ArrayToBase64 } from './tools'; import { ProcessingFormDialog } from './dialogs/ProcessingFormDialog'; interface ICreateEntry { @@ -1423,18 +1423,18 @@ export function addCommands( const sources = model.sharedModel.sources ?? {}; const exportSchema = { - ...(formSchemaRegistry.getSchemas().get('ExportGeoTIFFSchema') as IDict) + ...(formSchemaRegistry.getSchemas().get('Rasterize') as IDict) }; console.log(exportSchema); const formValues = await new Promise(resolve => { const dialog = new ProcessingFormDialog({ - title: 'Download GeoTIFF', + title: 'Rasterize Layer', schema: exportSchema, model, formContext: 'create', processingType: 'export', - sourceData: { resolutionX: 1200, resolutionY: 1200 }, + sourceData: { resolutionX: 1200, resolutionY: 1200, outputLayerName: selectedLayer.name + ' Rasterized', }, syncData: (props: IDict) => { resolve(props); dialog.dispose(); @@ -1448,7 +1448,7 @@ export function addCommands( return; } - const exportFileName = formValues.exportFileName; + const outputFileName = formValues.outputLayerName; const resolutionX = formValues.resolutionX ?? 1200; const resolutionY = formValues.resolutionY ?? 1200; const sourceId = selectedLayer.parameters.source; @@ -1496,7 +1496,7 @@ export function addCommands( const base64String = uint8ArrayToBase64(exportedBytes); - const savePath = `examples/${exportFileName}.tif`; + const savePath = `examples/${outputFileName}.tif`; await app.serviceManager.contents.save(savePath, { type: 'file', @@ -1505,6 +1505,24 @@ export function addCommands( }); Gdal.close(dataset); + + // Store in shared model + const newSourceId = UUID.uuid4(); + const sourceModel: IJGISSource = { + type: 'GeoTiffSource', + name: outputFileName, + parameters: { urls: [{url: savePath, min: 0, max: 1000}] } + }; + + const layerModel: IJGISLayer = { + type: 'RasterLayer', + parameters: { source: newSourceId }, + visible: true, + name: outputFileName + }; + + model.sharedModel.addSource(newSourceId, sourceModel); + model.addLayer(UUID.uuid4(), layerModel); } }); diff --git a/packages/base/src/dialogs/ProcessingFormDialog.tsx b/packages/base/src/dialogs/ProcessingFormDialog.tsx index ff2a31a47..95749df04 100644 --- a/packages/base/src/dialogs/ProcessingFormDialog.tsx +++ b/packages/base/src/dialogs/ProcessingFormDialog.tsx @@ -86,8 +86,6 @@ export class ProcessingFormDialog extends Dialog { // Modify schema to include layer options and layer name field if (options.schema) { - console.log(options.schema.properties?.inputLayer); - if (options.schema.properties?.inputLayer) { options.schema.properties.inputLayer.enum = layerOptions.map( option => option.value diff --git a/packages/schema/src/schema/export/exportGeotiff.json b/packages/schema/src/schema/export/exportGeotiff.json index 4df6ace8c..56fd3ede9 100644 --- a/packages/schema/src/schema/export/exportGeotiff.json +++ b/packages/schema/src/schema/export/exportGeotiff.json @@ -1,16 +1,10 @@ { "type": "object", - "description": "ExportGeoTIFFSchema", + "description": "Rasterize", "title": "IExportGeoTIFF", - "required": ["exportFileName", "resolutionX", "resolutionY"], + "required": ["resolutionX", "resolutionY"], "additionalProperties": false, "properties": { - "exportFileName": { - "type": "string", - "title": "GeoTiFF File Name", - "default": "exported_layer", - "description": "The name of the exported GeoTIFF file." - }, "resolutionX": { "type": "number", "title": "Resolution (Width)", From 2fad1eec7260efc63126565a97b87781f38382d8 Mon Sep 17 00:00:00 2001 From: arjxn-py Date: Fri, 28 Mar 2025 17:52:19 +0530 Subject: [PATCH 3/3] path handling --- packages/base/src/commands.ts | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/packages/base/src/commands.ts b/packages/base/src/commands.ts index 7c35370d8..2482cc85e 100644 --- a/packages/base/src/commands.ts +++ b/packages/base/src/commands.ts @@ -26,7 +26,11 @@ import keybindings from './keybindings.json'; import { JupyterGISTracker } from './types'; import { JupyterGISDocumentWidget } from './widget'; import { getGdal } from './gdal'; -import { getGeoJSONDataFromLayerSource, downloadFile, uint8ArrayToBase64 } from './tools'; +import { + getGeoJSONDataFromLayerSource, + downloadFile, + uint8ArrayToBase64 +} from './tools'; import { ProcessingFormDialog } from './dialogs/ProcessingFormDialog'; interface ICreateEntry { @@ -1434,7 +1438,11 @@ export function addCommands( model, formContext: 'create', processingType: 'export', - sourceData: { resolutionX: 1200, resolutionY: 1200, outputLayerName: selectedLayer.name + ' Rasterized', }, + sourceData: { + resolutionX: 1200, + resolutionY: 1200, + outputLayerName: selectedLayer.name + ' Rasterized' + }, syncData: (props: IDict) => { resolve(props); dialog.dispose(); @@ -1495,8 +1503,15 @@ export function addCommands( const base64String = uint8ArrayToBase64(exportedBytes); + const jgisFilePath = tracker.currentWidget?.model.filePath; + const jgisDir = jgisFilePath + ? jgisFilePath.substring(0, jgisFilePath.lastIndexOf('/')) + : ''; - const savePath = `examples/${outputFileName}.tif`; + // Ensure the path is valid and construct the save path + const savePath = jgisDir + ? `${jgisDir}/${outputFileName}.tif` + : `${outputFileName}.tif`; await app.serviceManager.contents.save(savePath, { type: 'file', @@ -1511,11 +1526,13 @@ export function addCommands( const sourceModel: IJGISSource = { type: 'GeoTiffSource', name: outputFileName, - parameters: { urls: [{url: savePath, min: 0, max: 1000}] } + parameters: { + urls: [{ url: `${outputFileName}.tif`, min: 0, max: 1000 }] + } }; const layerModel: IJGISLayer = { - type: 'RasterLayer', + type: 'WebGlLayer', parameters: { source: newSourceId }, visible: true, name: outputFileName