diff --git a/packages/base/src/commands/BaseCommandIDs.ts b/packages/base/src/commands/BaseCommandIDs.ts index 2a511d6d3..422280cb8 100644 --- a/packages/base/src/commands/BaseCommandIDs.ts +++ b/packages/base/src/commands/BaseCommandIDs.ts @@ -18,15 +18,15 @@ export const getGeolocation = 'jupytergis:getGeolocation'; export const openLayerBrowser = 'jupytergis:openLayerBrowser'; // Layer and source -export const newRasterEntry = 'jupytergis:newRasterEntry'; -export const newVectorTileEntry = 'jupytergis:newVectorTileEntry'; -export const newShapefileEntry = 'jupytergis:newShapefileEntry'; -export const newGeoJSONEntry = 'jupytergis:newGeoJSONEntry'; -export const newHillshadeEntry = 'jupytergis:newHillshadeEntry'; -export const newImageEntry = 'jupytergis:newImageEntry'; -export const newVideoEntry = 'jupytergis:newVideoEntry'; -export const newGeoTiffEntry = 'jupytergis:newGeoTiffEntry'; -export const newGeoParquetEntry = 'jupytergis:newGeoParquetEntry'; +export const opeNewRasterDialog = 'jupytergis:opeNewRasterDialog'; +export const openNewVectorTileDialog = 'jupytergis:openNewVectorTileDialog'; +export const openNewShapefileDialog = 'jupytergis:openNewShapefileDialog'; +export const openNewGeoJSONDialog = 'jupytergis:openNewGeoJSONDialog'; +export const openNewHillshadeDialog = 'jupytergis:openNewHillshadeDialog'; +export const openNewImageDialog = 'jupytergis:openNewImageDialog'; +export const openNewVideoDialog = 'jupytergis:openNewVideoDialog'; +export const openNewGeoTiffDialog = 'jupytergis:openNewGeoTiffDialog'; +export const openNewGeoParquetDialog = 'jupytergis:openNewGeoParquetDialog'; // Layer and group actions export const renameLayer = 'jupytergis:renameLayer'; diff --git a/packages/base/src/commands/index.ts b/packages/base/src/commands/index.ts index 807241d6b..de2094cd3 100644 --- a/packages/base/src/commands/index.ts +++ b/packages/base/src/commands/index.ts @@ -32,6 +32,8 @@ import { addProcessingCommands } from '../processing/processingCommands'; import { getGeoJSONDataFromLayerSource, downloadFile } from '../tools'; import { JupyterGISTracker } from '../types'; import { JupyterGISDocumentWidget } from '../widget'; +import { addLayerCreationCommands } from './operationCommands'; +import { addProcessingCommandsFromParams } from './processingCommandsFromParams'; const POINT_SELECTION_TOOL_CLASS = 'jGIS-point-selection-tool'; @@ -72,8 +74,21 @@ export function addCommands( const trans = translator.load('jupyterlab'); const { commands } = app; + addLayerCreationCommands({ tracker, commands, trans }); + commands.addCommand(CommandIDs.symbology, { label: trans.__('Edit Symbology'), + describedBy: { + args: { + type: 'object', + properties: { + selected: { + type: 'object', + description: 'Currently selected layer(s) in the map view', + }, + }, + }, + }, isEnabled: () => { const model = tracker.currentWidget?.model; const localState = model?.sharedModel.awareness.getLocalState(); @@ -112,13 +127,29 @@ export function addCommands( commands.addCommand(CommandIDs.redo, { label: trans.__('Redo'), + describedBy: { + args: { + type: 'object', + properties: { + filePath: { + type: 'string', + description: + 'Optional .jGIS file path. If omitted, uses active widget.', + }, + }, + }, + }, isEnabled: () => { return tracker.currentWidget ? tracker.currentWidget.model.sharedModel.editable : false; }, - execute: () => { - const current = tracker.currentWidget; + execute: (args?: { filePath?: string }) => { + const filePath = args?.filePath; + + const current = filePath + ? tracker.find(w => w.model.filePath === filePath) + : tracker.currentWidget; if (current) { return current.model.sharedModel.redo(); @@ -129,13 +160,30 @@ export function addCommands( commands.addCommand(CommandIDs.undo, { label: trans.__('Undo'), + describedBy: { + args: { + type: 'object', + required: [], + properties: { + filePath: { + type: 'string', + description: + 'Optional .jGIS file path. If omitted, uses active widget.', + }, + }, + }, + }, isEnabled: () => { return tracker.currentWidget ? tracker.currentWidget.model.sharedModel.editable : false; }, - execute: () => { - const current = tracker.currentWidget; + execute: (args: { filePath?: string }) => { + const filePath = args?.filePath; + + const current = filePath + ? tracker.find(w => w.model.filePath === filePath) + : tracker.currentWidget; if (current) { return current.model.sharedModel.undo(); @@ -146,6 +194,20 @@ export function addCommands( commands.addCommand(CommandIDs.identify, { label: trans.__('Identify'), + describedBy: { + args: { + type: 'object', + required: [], + properties: { + filePath: { + type: 'string', + description: + 'Optional .jGIS file path. If omitted, uses active widget.', + }, + }, + }, + }, + isToggled: () => { const current = tracker.currentWidget; if (!current) { @@ -156,6 +218,7 @@ export function addCommands( if (!selectedLayer) { return false; } + const canIdentify = [ 'VectorLayer', 'ShapefileLayer', @@ -186,8 +249,14 @@ export function addCommands( 'VectorTileLayer', ].includes(selectedLayer.type); }, + execute: args => { - const current = tracker.currentWidget; + const filePath = args?.filePath; + + const current = filePath + ? tracker.find(w => w.model.filePath === filePath) + : tracker.currentWidget; + if (!current) { return; } @@ -216,6 +285,17 @@ export function addCommands( commands.addCommand(CommandIDs.temporalController, { label: trans.__('Temporal Controller'), + describedBy: { + args: { + type: 'object', + properties: { + filePath: { + type: 'string', + description: 'Optional path to the .jGIS file', + }, + }, + }, + }, isToggled: () => { return tracker.currentWidget?.model.isTemporalControllerActive || false; }, @@ -251,8 +331,14 @@ export function addCommands( return true; }, - execute: () => { - const current = tracker.currentWidget; + + execute: (args?: { filePath?: string }) => { + const filePath = args?.filePath; + + const current = filePath + ? tracker.find(w => w.model.filePath === filePath) + : tracker.currentWidget; + if (!current) { return; } @@ -268,6 +354,12 @@ export function addCommands( */ commands.addCommand(CommandIDs.openLayerBrowser, { label: trans.__('Open Layer Browser'), + describedBy: { + args: { + type: 'object', + properties: {}, + }, + }, isEnabled: () => { return tracker.currentWidget ? tracker.currentWidget.model.sharedModel.editable @@ -284,8 +376,14 @@ export function addCommands( /** * Source and layers */ - commands.addCommand(CommandIDs.newRasterEntry, { - label: trans.__('New Raster Tile Layer'), + commands.addCommand(CommandIDs.opeNewRasterDialog, { + label: trans.__('Open New Raster Tile Dialog'), + describedBy: { + args: { + type: 'object', + properties: {}, + }, + }, isEnabled: () => { return tracker.currentWidget ? tracker.currentWidget.model.sharedModel.editable @@ -305,11 +403,17 @@ export function addCommands( sourceType: 'RasterSource', layerType: 'RasterLayer', }), - ...icons.get(CommandIDs.newRasterEntry), + ...icons.get(CommandIDs.opeNewRasterDialog), }); - commands.addCommand(CommandIDs.newVectorTileEntry, { - label: trans.__('New Vector Tile Layer'), + commands.addCommand(CommandIDs.openNewVectorTileDialog, { + label: trans.__('Open New Vector Tile Dialog'), + describedBy: { + args: { + type: 'object', + properties: {}, + }, + }, isEnabled: () => { return tracker.currentWidget ? tracker.currentWidget.model.sharedModel.editable @@ -326,11 +430,17 @@ export function addCommands( sourceType: 'VectorTileSource', layerType: 'VectorTileLayer', }), - ...icons.get(CommandIDs.newVectorTileEntry), + ...icons.get(CommandIDs.openNewVectorTileDialog), }); - commands.addCommand(CommandIDs.newGeoParquetEntry, { - label: trans.__('New GeoParquet Layer'), + commands.addCommand(CommandIDs.openNewGeoParquetDialog, { + label: trans.__('Open New GeoParquet Dialog'), + describedBy: { + args: { + type: 'object', + properties: {}, + }, + }, isEnabled: () => { return tracker.currentWidget ? tracker.currentWidget.model.sharedModel.editable @@ -347,11 +457,17 @@ export function addCommands( sourceType: 'GeoParquetSource', layerType: 'VectorLayer', }), - ...icons.get(CommandIDs.newGeoParquetEntry), + ...icons.get(CommandIDs.openNewGeoParquetDialog), }); - commands.addCommand(CommandIDs.newGeoJSONEntry, { - label: trans.__('New GeoJSON layer'), + commands.addCommand(CommandIDs.openNewGeoJSONDialog, { + label: trans.__('Open New GeoJSON Dialog'), + describedBy: { + args: { + type: 'object', + properties: {}, + }, + }, isEnabled: () => { return tracker.currentWidget ? tracker.currentWidget.model.sharedModel.editable @@ -367,14 +483,28 @@ export function addCommands( sourceType: 'GeoJSONSource', layerType: 'VectorLayer', }), - ...icons.get(CommandIDs.newGeoJSONEntry), + ...icons.get(CommandIDs.openNewGeoJSONDialog), }); //Add processing commands addProcessingCommands(app, commands, tracker, trans, formSchemaRegistry); + addProcessingCommandsFromParams({ + app, + commands, + tracker, + trans, + formSchemaRegistry, + processingSchemas: Object.fromEntries(formSchemaRegistry.getSchemas()), + }); - commands.addCommand(CommandIDs.newHillshadeEntry, { - label: trans.__('New Hillshade layer'), + commands.addCommand(CommandIDs.openNewHillshadeDialog, { + label: trans.__('Open New Hillshade Dialog'), + describedBy: { + args: { + type: 'object', + properties: {}, + }, + }, isEnabled: () => { return tracker.currentWidget ? tracker.currentWidget.model.sharedModel.editable @@ -390,11 +520,17 @@ export function addCommands( sourceType: 'RasterDemSource', layerType: 'HillshadeLayer', }), - ...icons.get(CommandIDs.newHillshadeEntry), + ...icons.get(CommandIDs.openNewHillshadeDialog), }); - commands.addCommand(CommandIDs.newImageEntry, { - label: trans.__('New Image layer'), + commands.addCommand(CommandIDs.openNewImageDialog, { + label: trans.__('Open New Image Dialog'), + describedBy: { + args: { + type: 'object', + properties: {}, + }, + }, isEnabled: () => { return tracker.currentWidget ? tracker.currentWidget.model.sharedModel.editable @@ -420,11 +556,17 @@ export function addCommands( sourceType: 'ImageSource', layerType: 'ImageLayer', }), - ...icons.get(CommandIDs.newImageEntry), + ...icons.get(CommandIDs.openNewImageDialog), }); - commands.addCommand(CommandIDs.newVideoEntry, { - label: trans.__('New Video layer'), + commands.addCommand(CommandIDs.openNewVideoDialog, { + label: trans.__('Open New Video Dialog'), + describedBy: { + args: { + type: 'object', + properties: {}, + }, + }, isEnabled: () => { return tracker.currentWidget ? tracker.currentWidget.model.sharedModel.editable @@ -453,11 +595,17 @@ export function addCommands( sourceType: 'VideoSource', layerType: 'RasterLayer', }), - ...icons.get(CommandIDs.newVideoEntry), + ...icons.get(CommandIDs.openNewVideoDialog), }); - commands.addCommand(CommandIDs.newGeoTiffEntry, { - label: trans.__('New GeoTiff layer'), + commands.addCommand(CommandIDs.openNewGeoTiffDialog, { + label: trans.__('Open New GeoTiff Dialog'), + describedBy: { + args: { + type: 'object', + properties: {}, + }, + }, isEnabled: () => { return tracker.currentWidget ? tracker.currentWidget.model.sharedModel.editable @@ -477,11 +625,17 @@ export function addCommands( sourceType: 'GeoTiffSource', layerType: 'WebGlLayer', }), - ...icons.get(CommandIDs.newGeoTiffEntry), + ...icons.get(CommandIDs.openNewGeoTiffDialog), }); - commands.addCommand(CommandIDs.newShapefileEntry, { - label: trans.__('New Shapefile Layer'), + commands.addCommand(CommandIDs.openNewShapefileDialog, { + label: trans.__('Open New Shapefile Dialog'), + describedBy: { + args: { + type: 'object', + properties: {}, + }, + }, isEnabled: () => { return tracker.currentWidget ? tracker.currentWidget.model.sharedModel.editable @@ -498,7 +652,7 @@ export function addCommands( sourceType: 'ShapefileSource', layerType: 'VectorLayer', }), - ...icons.get(CommandIDs.newShapefileEntry), + ...icons.get(CommandIDs.openNewShapefileDialog), }); /** @@ -506,16 +660,98 @@ export function addCommands( */ commands.addCommand(CommandIDs.renameLayer, { label: trans.__('Rename Layer'), - execute: async () => { - const model = tracker.currentWidget?.model; + describedBy: { + args: { + type: 'object', + properties: { + filePath: { + type: 'string', + description: 'Optional path to the .jGIS file', + }, + layerId: { + type: 'string', + description: 'Optional ID of the layer to rename', + }, + newName: { + type: 'string', + description: 'Optional new name for the layer', + }, + }, + }, + }, + + execute: async (args?: { + filePath?: string; + layerId?: string; + newName?: string; + }) => { + const { filePath, layerId, newName } = args ?? {}; + + const model = filePath + ? tracker.find(w => w.model.filePath === filePath)?.model + : tracker.currentWidget?.model; + + if (!model || !model.sharedModel.editable) { + return; + } + + // ---- PARAMETER MODE ---- + // If all args are present, use them + if (filePath && layerId && newName) { + const layer = model.sharedModel.layers[layerId]; + if (!layer) { + return; + } + layer.name = newName; + model.sharedModel.updateLayer(layerId, layer); + return; + } + + // ---- FALLBACK TO ORIGINAL INTERACTIVE BEHAVIOR ---- await Private.renameSelectedItem(model, 'layer'); }, }); commands.addCommand(CommandIDs.removeLayer, { label: trans.__('Remove Layer'), - execute: () => { - const model = tracker.currentWidget?.model; + describedBy: { + args: { + type: 'object', + properties: { + filePath: { + type: 'string', + description: 'Optional path to the .jGIS file', + }, + layerId: { + type: 'string', + description: 'Optional ID of the layer to remove', + }, + }, + }, + }, + + execute: (args?: { filePath?: string; layerId?: string }) => { + const { filePath, layerId } = args ?? {}; + + const model = filePath + ? tracker.find(w => w.model.filePath === filePath)?.model + : tracker.currentWidget?.model; + + if (!model || !model.sharedModel.editable) { + return; + } + + // ---- PARAMETER MODE ---- + if (filePath && layerId) { + const exists = model.sharedModel.layers[layerId]; + if (!exists) { + return; + } + model.removeLayer(layerId); + return; + } + + // ---- FALLBACK TO ORIGINAL INTERACTIVE BEHAVIOR ---- Private.removeSelectedItems(model, 'layer', selection => { model?.removeLayer(selection); }); @@ -524,17 +760,89 @@ export function addCommands( commands.addCommand(CommandIDs.renameGroup, { label: trans.__('Rename Group'), - execute: async () => { - const model = tracker.currentWidget?.model; + describedBy: { + args: { + type: 'object', + properties: { + filePath: { + type: 'string', + description: 'Optional .jGIS file path', + }, + oldName: { + type: 'string', + description: 'Optional existing group name', + }, + newName: { + type: 'string', + description: 'Optional new group name', + }, + }, + }, + }, + + execute: async (args?: { + filePath?: string; + oldName?: string; + newName?: string; + }) => { + const { filePath, oldName, newName } = args ?? {}; + + const model = filePath + ? tracker.find(w => w.model.filePath === filePath)?.model + : tracker.currentWidget?.model; + + if (!model || !model.sharedModel.editable) { + return; + } + + // ---- PARAMETER MODE ---- + if (filePath && oldName && newName) { + model.renameLayerGroup(oldName, newName); + return; + } + + // ---- FALLBACK TO ORIGINAL INTERACTIVE BEHAVIOR ---- await Private.renameSelectedItem(model, 'group'); }, }); commands.addCommand(CommandIDs.removeGroup, { label: trans.__('Remove Group'), - execute: async () => { - const model = tracker.currentWidget?.model; - Private.removeSelectedItems(model, 'group', selection => { + describedBy: { + args: { + type: 'object', + properties: { + filePath: { + type: 'string', + description: 'Optional .jGIS file path', + }, + groupName: { + type: 'string', + description: 'Optional group name to remove', + }, + }, + }, + }, + + execute: async (args?: { filePath?: string; groupName?: string }) => { + const { filePath, groupName } = args ?? {}; + + const model = filePath + ? tracker.find(w => w.model.filePath === filePath)?.model + : tracker.currentWidget?.model; + + if (!model || !model.sharedModel.editable) { + return; + } + + // ---- PARAMETER MODE ---- + if (filePath && groupName) { + model.removeLayerGroup(groupName); + return; + } + + // ---- FALLBACK TO ORIGINAL INTERACTIVE BEHAVIOR ---- + await Private.removeSelectedItems(model, 'group', selection => { model?.removeLayerGroup(selection); }); }, @@ -543,26 +851,104 @@ export function addCommands( commands.addCommand(CommandIDs.moveLayersToGroup, { label: args => args['label'] ? (args['label'] as string) : trans.__('Move to Root'), - execute: args => { - const model = tracker.currentWidget?.model; - const groupName = args['label'] as string; + describedBy: { + args: { + type: 'object', + properties: { + label: { type: 'string' }, + filePath: { type: 'string' }, + layerIds: { + type: 'array', + items: { type: 'string' }, + }, + groupName: { type: 'string' }, + }, + }, + }, + + execute: (args?: { + filePath?: string; + layerIds?: string[]; + groupName?: string; + label?: string; + }) => { + const { filePath, layerIds, groupName } = args ?? {}; - const selectedLayers = model?.localState?.selected?.value; + // Resolve model based on filePath or current widget + const model = filePath + ? tracker.find(w => w.model.filePath === filePath)?.model + : tracker.currentWidget?.model; + if (!model || !model.sharedModel.editable) { + return; + } + + // ---- PARAMETER MODE ---- + if (filePath && layerIds && groupName !== undefined) { + model.moveItemsToGroup(layerIds, groupName); + return; + } + + // ---- FALLBACK TO ORIGINAL INTERACTIVE BEHAVIOR ---- + const selectedLayers = model.localState?.selected?.value; if (!selectedLayers) { return; } - model.moveItemsToGroup(Object.keys(selectedLayers), groupName); + const targetGroup = args?.label as string; + model.moveItemsToGroup(Object.keys(selectedLayers), targetGroup); }, }); commands.addCommand(CommandIDs.moveLayerToNewGroup, { label: trans.__('Move Selected Layers to New Group'), - execute: async () => { - const model = tracker.currentWidget?.model; - const selectedLayers = model?.localState?.selected?.value; + describedBy: { + args: { + type: 'object', + properties: { + filePath: { type: 'string' }, + groupName: { type: 'string' }, + layerIds: { + type: 'array', + items: { type: 'string' }, + }, + }, + }, + }, + + execute: async (args?: { + filePath?: string; + groupName?: string; + layerIds?: string[]; + }) => { + const { filePath, groupName, layerIds } = args ?? {}; + + const model = filePath + ? tracker.find(w => w.model.filePath === filePath)?.model + : tracker.currentWidget?.model; + + if (!model || !model.sharedModel.editable) { + return; + } + // ---- PARAMETER MODE ---- + if (filePath && groupName && layerIds) { + const layerMap: { [key: string]: any } = {}; + layerIds.forEach(id => { + layerMap[id] = { type: 'layer', selectedNodeId: id }; + }); + + const newGroup: IJGISLayerGroup = { + name: groupName, + layers: layerIds, + }; + + model.addNewLayerGroup(layerMap, newGroup); + return; + } + + // ---- FALLBACK TO ORIGINAL INTERACTIVE BEHAVIOR ---- + const selectedLayers = model.localState?.selected?.value; if (!selectedLayers) { return; } @@ -625,16 +1011,89 @@ export function addCommands( */ commands.addCommand(CommandIDs.renameSource, { label: trans.__('Rename Source'), - execute: async () => { - const model = tracker.currentWidget?.model; + describedBy: { + args: { + type: 'object', + properties: { + filePath: { type: 'string' }, + sourceId: { type: 'string' }, + newName: { type: 'string' }, + }, + }, + }, + + execute: async (args?: { + filePath?: string; + sourceId?: string; + newName?: string; + }) => { + const { filePath, sourceId, newName } = args ?? {}; + + const model = filePath + ? tracker.find(w => w.model.filePath === filePath)?.model + : tracker.currentWidget?.model; + + if (!model || !model.sharedModel.editable) { + return; + } + + // ---- PARAMETER MODE ---- + if (filePath && sourceId && newName) { + const source = model.getSource(sourceId); + if (!source) { + console.warn(`Source with ID ${sourceId} not found`); + return; + } + + source.name = newName; + model.sharedModel.updateSource(sourceId, source); + return; + } + + // ---- FALLBACK TO ORIGINAL INTERACTIVE BEHAVIOR ---- await Private.renameSelectedItem(model, 'source'); }, }); commands.addCommand(CommandIDs.removeSource, { label: trans.__('Remove Source'), - execute: () => { - const model = tracker.currentWidget?.model; + describedBy: { + args: { + type: 'object', + properties: { + filePath: { type: 'string' }, + sourceId: { type: 'string' }, + }, + }, + }, + + execute: (args?: { filePath?: string; sourceId?: string }) => { + const { filePath, sourceId } = args ?? {}; + + const model = filePath + ? tracker.find(w => w.model.filePath === filePath)?.model + : tracker.currentWidget?.model; + + if (!model || !model.sharedModel.editable) { + return; + } + + // ---- PARAMETER MODE ---- + if (filePath && sourceId) { + const layersUsingSource = model.getLayersBySource(sourceId); + if (layersUsingSource.length > 0) { + showErrorMessage( + 'Remove source error', + 'The source is used by a layer.', + ); + return; + } + + model.sharedModel.removeSource(sourceId); + return; + } + + // ---- FALLBACK TO ORIGINAL INTERACTIVE BEHAVIOR ---- Private.removeSelectedItems(model, 'source', selection => { if (!(model?.getLayersBySource(selection).length ?? true)) { model?.sharedModel.removeSource(selection); @@ -651,6 +1110,12 @@ export function addCommands( // Console commands commands.addCommand(CommandIDs.toggleConsole, { label: trans.__('Toggle console'), + describedBy: { + args: { + type: 'object', + properties: {}, + }, + }, isVisible: () => tracker.currentWidget instanceof JupyterGISDocumentWidget, isEnabled: () => { return tracker.currentWidget @@ -669,8 +1134,15 @@ export function addCommands( commands.notifyCommandChanged(CommandIDs.toggleConsole); }, }); + commands.addCommand(CommandIDs.executeConsole, { label: trans.__('Execute console'), + describedBy: { + args: { + type: 'object', + properties: {}, + }, + }, isVisible: () => tracker.currentWidget instanceof JupyterGISDocumentWidget, isEnabled: () => { return tracker.currentWidget @@ -679,8 +1151,15 @@ export function addCommands( }, execute: () => Private.executeConsole(tracker), }); + commands.addCommand(CommandIDs.removeConsole, { label: trans.__('Remove console'), + describedBy: { + args: { + type: 'object', + properties: {}, + }, + }, isVisible: () => tracker.currentWidget instanceof JupyterGISDocumentWidget, isEnabled: () => { return tracker.currentWidget @@ -692,6 +1171,12 @@ export function addCommands( commands.addCommand(CommandIDs.invokeCompleter, { label: trans.__('Display the completion helper.'), + describedBy: { + args: { + type: 'object', + properties: {}, + }, + }, isVisible: () => tracker.currentWidget instanceof JupyterGISDocumentWidget, execute: () => { const currentWidget = tracker.currentWidget; @@ -711,6 +1196,12 @@ export function addCommands( commands.addCommand(CommandIDs.selectCompleter, { label: trans.__('Select the completion suggestion.'), + describedBy: { + args: { + type: 'object', + properties: {}, + }, + }, isVisible: () => tracker.currentWidget instanceof JupyterGISDocumentWidget, execute: () => { const currentWidget = tracker.currentWidget; @@ -730,33 +1221,118 @@ export function addCommands( commands.addCommand(CommandIDs.zoomToLayer, { label: trans.__('Zoom to Layer'), - execute: () => { - const currentWidget = tracker.currentWidget; - if (!currentWidget || !completionProviderManager) { + describedBy: { + args: { + type: 'object', + properties: { + filePath: { type: 'string' }, + layerId: { type: 'string' }, + }, + }, + }, + + execute: (args?: { filePath?: string; layerId?: string }) => { + const { filePath, layerId } = args ?? {}; + + // Determine model from provided file path or fallback to current widget + const current = filePath + ? tracker.find(w => w.model.filePath === filePath) + : tracker.currentWidget; + + if (!current || !current.model.sharedModel.editable) { + return; + } + + const model = current.model; + + // ----- PARAMETER MODE ----- + if (filePath && layerId) { + console.log(`Zooming to layer: ${layerId}`); + model.centerOnPosition(layerId); return; } - console.log('zooming'); - const model = tracker.currentWidget.model; - const selectedItems = model.localState?.selected.value; + // ----- FALLBACK TO ORIGINAL INTERACTIVE BEHAVIOR ----- + const selectedItems = model.localState?.selected?.value; if (!selectedItems) { return; } - const layerId = Object.keys(selectedItems)[0]; - model.centerOnPosition(layerId); + const selLayerId = Object.keys(selectedItems)[0]; + console.log('zooming'); + model.centerOnPosition(selLayerId); }, }); commands.addCommand(CommandIDs.downloadGeoJSON, { label: trans.__('Download as GeoJSON'), + describedBy: { + args: { + type: 'object', + properties: { + filePath: { type: 'string' }, + layerId: { type: 'string' }, + exportFileName: { type: 'string' }, + }, + }, + }, isEnabled: () => { const selectedLayer = getSingleSelectedLayer(tracker); return selectedLayer ? ['VectorLayer', 'ShapefileLayer'].includes(selectedLayer.type) : false; }, - execute: async () => { + + execute: async (args?: { + filePath?: string; + layerId?: string; + exportFileName?: string; + }) => { + const { filePath, layerId, exportFileName } = args ?? {}; + + // ----- PARAMETER MODE ----- + if (filePath && layerId && exportFileName) { + const current = tracker.find(w => w.model.filePath === filePath); + + if (!current || !current.model.sharedModel.editable) { + console.warn('Invalid or non-editable document'); + return; + } + + const model = current.model; + const layer = model.getLayer(layerId); + + if (!layer || !['VectorLayer', 'ShapefileLayer'].includes(layer.type)) { + console.warn('Layer type not supported for GeoJSON export'); + return; + } + + const sources = model.sharedModel.sources ?? {}; + const sourceId = layer.parameters?.source; + const source = sources[sourceId]; + if (!source) { + console.warn('Source not found for selected layer'); + return; + } + + const geojsonString = await getGeoJSONDataFromLayerSource( + source, + model, + ); + if (!geojsonString) { + console.warn('Failed to generate GeoJSON data'); + return; + } + + downloadFile( + geojsonString, + `${exportFileName}.geojson`, + 'application/geo+json', + ); + return; + } + + // ----- FALLBACK TO ORIGINAL INTERACTIVE BEHAVIOR ----- const selectedLayer = getSingleSelectedLayer(tracker); if (!selectedLayer) { return; @@ -791,7 +1367,7 @@ export function addCommands( return; } - const exportFileName = formValues.exportFileName; + const outName = formValues.exportFileName; const sourceId = selectedLayer.parameters.source; const source = sources[sourceId]; @@ -800,17 +1376,62 @@ export function addCommands( return; } - downloadFile( - geojsonString, - `${exportFileName}.geojson`, - 'application/geo+json', - ); + downloadFile(geojsonString, `${outName}.geojson`, 'application/geo+json'); }, }); commands.addCommand(CommandIDs.getGeolocation, { label: trans.__('Center on Geolocation'), - execute: async () => { + describedBy: { + args: { + type: 'object', + properties: { + filePath: { type: 'string' }, + }, + }, + }, + + execute: async (args?: { filePath?: string }) => { + const { filePath } = args ?? {}; + + // ----- PARAMETER MODE ----- + if (filePath) { + const current = tracker.find(w => w.model.filePath === filePath); + if (!current) { + console.warn('No document found for provided filePath'); + return; + } + + const viewModel = current.model; + const options = { + enableHighAccuracy: true, + timeout: 5000, + maximumAge: 0, + }; + + const success = (pos: GeolocationPosition) => { + const location: Coordinate = fromLonLat([ + pos.coords.longitude, + pos.coords.latitude, + ]); + + const jgisLocation: JgisCoordinates = { + x: location[0], + y: location[1], + }; + + viewModel.geolocationChanged.emit(jgisLocation); + }; + + const error = (err: GeolocationPositionError) => { + console.warn(`Geolocation error (${err.code}): ${err.message}`); + }; + + navigator.geolocation.getCurrentPosition(success, error, options); + return; + } + + // ----- FALLBACK TO ORIGINAL INTERACTIVE BEHAVIOR ----- const viewModel = tracker.currentWidget?.model; const options = { enableHighAccuracy: true, @@ -841,6 +1462,12 @@ export function addCommands( // Panel visibility commands commands.addCommand(CommandIDs.toggleLeftPanel, { label: trans.__('Toggle Left Panel'), + describedBy: { + args: { + type: 'object', + properties: {}, + }, + }, isEnabled: () => Boolean(tracker.currentWidget), isToggled: () => { const current = tracker.currentWidget; @@ -868,6 +1495,12 @@ export function addCommands( commands.addCommand(CommandIDs.toggleRightPanel, { label: trans.__('Toggle Right Panel'), + describedBy: { + args: { + type: 'object', + properties: {}, + }, + }, isEnabled: () => Boolean(tracker.currentWidget), isToggled: () => { const current = tracker.currentWidget; @@ -896,6 +1529,12 @@ export function addCommands( // Left panel tabs commands.addCommand(CommandIDs.showLayersTab, { label: trans.__('Show Layers Tab'), + describedBy: { + args: { + type: 'object', + properties: {}, + }, + }, isEnabled: () => Boolean(tracker.currentWidget), isToggled: () => tracker.currentWidget @@ -918,6 +1557,12 @@ export function addCommands( commands.addCommand(CommandIDs.showStacBrowserTab, { label: trans.__('Show STAC Browser Tab'), + describedBy: { + args: { + type: 'object', + properties: {}, + }, + }, isEnabled: () => Boolean(tracker.currentWidget), isToggled: () => tracker.currentWidget @@ -940,6 +1585,12 @@ export function addCommands( commands.addCommand(CommandIDs.showFiltersTab, { label: trans.__('Show Filters Tab'), + describedBy: { + args: { + type: 'object', + properties: {}, + }, + }, isEnabled: () => Boolean(tracker.currentWidget), isToggled: () => tracker.currentWidget @@ -963,6 +1614,12 @@ export function addCommands( // Right panel tabs commands.addCommand(CommandIDs.showObjectPropertiesTab, { label: trans.__('Show Object Properties Tab'), + describedBy: { + args: { + type: 'object', + properties: {}, + }, + }, isEnabled: () => Boolean(tracker.currentWidget), isToggled: () => tracker.currentWidget @@ -985,6 +1642,12 @@ export function addCommands( commands.addCommand(CommandIDs.showAnnotationsTab, { label: trans.__('Show Annotations Tab'), + describedBy: { + args: { + type: 'object', + properties: {}, + }, + }, isEnabled: () => Boolean(tracker.currentWidget), isToggled: () => tracker.currentWidget @@ -1007,6 +1670,12 @@ export function addCommands( commands.addCommand(CommandIDs.showIdentifyPanelTab, { label: trans.__('Show Identify Panel Tab'), + describedBy: { + args: { + type: 'object', + properties: {}, + }, + }, isEnabled: () => Boolean(tracker.currentWidget), isToggled: () => tracker.currentWidget diff --git a/packages/base/src/commands/operationCommands.ts b/packages/base/src/commands/operationCommands.ts new file mode 100644 index 000000000..f0390a349 --- /dev/null +++ b/packages/base/src/commands/operationCommands.ts @@ -0,0 +1,306 @@ +import { IJupyterGISModel, IJGISLayer, IJGISSource } from '@jupytergis/schema'; +import { IRenderMime } from '@jupyterlab/rendermime'; +import { CommandRegistry } from '@lumino/commands'; +import { UUID } from '@lumino/coreutils'; + +import { JupyterGISTracker } from '../types'; + +export namespace LayerCreationCommandIDs { + export const newGeoJSONWithParams = 'jupytergis:newGeoJSONWithParams'; + export const newRasterWithParams = 'jupytergis:newRasterWithParams'; + export const newVectorTileWithParams = 'jupytergis:newVectorTileWithParams'; + export const newGeoParquetWithParams = 'jupytergis:newGeoParquetWithParams'; + export const newHillshadeWithParams = 'jupytergis:newHillshadeWithParams'; + export const newImageWithParams = 'jupytergis:newImageWithParams'; + export const newVideoWithParams = 'jupytergis:newVideoWithParams'; + export const newGeoTiffWithParams = 'jupytergis:newGeoTiffWithParams'; + export const newShapefileWithParams = 'jupytergis:newShapefileWithParams'; +} + +type LayerCreationSpec = { + id: string; + label: string; + sourceType: string; + layerType: string; + sourceSchema: Record; + layerParamsSchema: Record; + buildParameters: (params: any, sourceId: string) => IJGISLayer['parameters']; +}; + +/** + * Generic command factory for layer creation. + */ +function createLayerCommand( + commands: CommandRegistry, + tracker: JupyterGISTracker, + trans: IRenderMime.TranslationBundle, + spec: LayerCreationSpec, +): void { + commands.addCommand(spec.id, { + label: trans.__(spec.label), + isEnabled: () => true, + describedBy: { + args: { + type: 'object', + required: ['filePath', 'Name', 'parameters'], + properties: { + filePath: { type: 'string', description: 'Path to the .jGIS file' }, + Name: { type: 'string', description: 'Layer name' }, + parameters: { + type: 'object', + properties: { + source: spec.sourceSchema, + ...spec.layerParamsSchema, + }, + }, + } as any, + }, + }, + execute: (async (args: { + filePath: string; + Name: string; + parameters: Record; + }) => { + const { filePath, Name, parameters } = args; + const current = tracker.find(w => w.model.filePath === filePath); + if (!current || !current.model.sharedModel.editable) { + console.warn('Invalid or non-editable document for', filePath); + return; + } + + const model: IJupyterGISModel = current.model; + const sharedModel = model.sharedModel; + const sourceId = UUID.uuid4(); + const layerId = UUID.uuid4(); + + const sourceModel: IJGISSource = { + type: spec.sourceType as any, + name: `${Name} Source`, + parameters: parameters.source, + }; + sharedModel.addSource(sourceId, sourceModel); + + const layerModel: IJGISLayer = { + type: spec.layerType as any, + name: Name, + visible: true, + parameters: spec.buildParameters(parameters, sourceId), + }; + model.addLayer(layerId, layerModel); + }) as any, + }); +} + +/** + * Register all layer creation commands using declarative specs. + */ +export function addLayerCreationCommands(options: { + tracker: JupyterGISTracker; + commands: CommandRegistry; + trans: IRenderMime.TranslationBundle; +}): void { + const { tracker, commands, trans } = options; + + const specs: LayerCreationSpec[] = [ + { + id: LayerCreationCommandIDs.newGeoJSONWithParams, + label: 'New GeoJSON Layer From Parameters', + sourceType: 'GeoJSONSource', + layerType: 'VectorLayer', + sourceSchema: { + type: 'object', + required: ['path'], + properties: { path: { type: 'string' } }, + }, + layerParamsSchema: { + color: { type: 'object' }, + opacity: { type: 'number', default: 1 }, + symbologyState: { type: 'object' }, + }, + buildParameters: (p, id) => ({ + source: id, + color: p.color ?? {}, + opacity: p.opacity ?? 1, + symbologyState: p.symbologyState, + }), + }, + { + id: LayerCreationCommandIDs.newRasterWithParams, + label: 'New Raster Layer From Parameters', + sourceType: 'RasterSource', + layerType: 'RasterLayer', + sourceSchema: { + type: 'object', + required: ['url'], + properties: { url: { type: 'string' } }, + }, + layerParamsSchema: { opacity: { type: 'number', default: 1 } }, + buildParameters: (p, id) => ({ + source: id, + opacity: p.opacity ?? 1, + }), + }, + { + id: LayerCreationCommandIDs.newVectorTileWithParams, + label: 'New Vector Tile Layer From Parameters', + sourceType: 'VectorTileSource', + layerType: 'VectorTileLayer', + sourceSchema: { + type: 'object', + required: ['url'], + properties: { url: { type: 'string' } }, + }, + layerParamsSchema: { + color: { type: 'object' }, + opacity: { type: 'number', default: 1 }, + }, + buildParameters: (p, id) => ({ + source: id, + color: p.color ?? {}, + opacity: p.opacity ?? 1, + }), + }, + { + id: LayerCreationCommandIDs.newGeoParquetWithParams, + label: 'New GeoParquet Layer From Parameters', + sourceType: 'GeoParquetSource', + layerType: 'VectorLayer', + sourceSchema: { + type: 'object', + required: ['path'], + properties: { path: { type: 'string' } }, + }, + layerParamsSchema: { + color: { type: 'object' }, + opacity: { type: 'number', default: 1 }, + symbologyState: { type: 'object' }, + }, + buildParameters: (p, id) => ({ + source: id, + color: p.color ?? {}, + opacity: p.opacity ?? 1, + symbologyState: p.symbologyState, + }), + }, + { + id: LayerCreationCommandIDs.newHillshadeWithParams, + label: 'New Hillshade Layer From Parameters', + sourceType: 'RasterDemSource', + layerType: 'HillshadeLayer', + sourceSchema: { + type: 'object', + required: ['url'], + properties: { url: { type: 'string' } }, + }, + layerParamsSchema: { + shadowColor: { type: 'string', default: '#473B24' }, + }, + buildParameters: (p, id) => ({ + source: id, + shadowColor: p.shadowColor ?? '#473B24', + }), + }, + { + id: LayerCreationCommandIDs.newImageWithParams, + label: 'New Image Layer From Parameters', + sourceType: 'ImageSource', + layerType: 'ImageLayer', + sourceSchema: { + type: 'object', + required: ['path', 'coordinates'], + properties: { + path: { type: 'string' }, + coordinates: { + type: 'array', + items: { type: 'array', items: { type: 'number' } }, + }, + }, + }, + layerParamsSchema: { opacity: { type: 'number', default: 1 } }, + buildParameters: (p, id) => ({ + source: id, + opacity: p.opacity ?? 1, + }), + }, + { + id: LayerCreationCommandIDs.newVideoWithParams, + label: 'New Video Layer From Parameters', + sourceType: 'VideoSource', + layerType: 'RasterLayer', + sourceSchema: { + type: 'object', + required: ['urls', 'coordinates'], + properties: { + urls: { type: 'array', items: { type: 'string' } }, + coordinates: { + type: 'array', + items: { type: 'array', items: { type: 'number' } }, + }, + }, + }, + layerParamsSchema: { opacity: { type: 'number', default: 1 } }, + buildParameters: (p, id) => ({ + source: id, + opacity: p.opacity ?? 1, + }), + }, + { + id: LayerCreationCommandIDs.newGeoTiffWithParams, + label: 'New GeoTIFF Layer From Parameters', + sourceType: 'GeoTiffSource', + layerType: 'WebGlLayer', + sourceSchema: { + type: 'object', + required: ['urls'], + properties: { + urls: { + type: 'array', + items: { + type: 'object', + properties: { + url: { type: 'string' }, + min: { type: 'number' }, + max: { type: 'number' }, + }, + }, + }, + }, + }, + layerParamsSchema: { + opacity: { type: 'number', default: 1 }, + color: { type: 'any' }, + symbologyState: { type: 'object' }, + }, + buildParameters: (p, id) => ({ + source: id, + opacity: p.opacity ?? 1, + color: p.color, + symbologyState: p.symbologyState ?? { renderType: 'continuous' }, + }), + }, + { + id: LayerCreationCommandIDs.newShapefileWithParams, + label: 'New Shapefile Layer From Parameters', + sourceType: 'ShapefileSource', + layerType: 'VectorLayer', + sourceSchema: { + type: 'object', + required: ['path'], + properties: { path: { type: 'string' } }, + }, + layerParamsSchema: { + color: { type: 'object' }, + opacity: { type: 'number', default: 1 }, + symbologyState: { type: 'object' }, + }, + buildParameters: (p, id) => ({ + source: id, + color: p.color ?? {}, + opacity: p.opacity ?? 1, + symbologyState: p.symbologyState ?? { renderType: 'Single Symbol' }, + }), + }, + ]; + + specs.forEach(spec => createLayerCommand(commands, tracker, trans, spec)); +} diff --git a/packages/base/src/commands/processingCommandsFromParams.ts b/packages/base/src/commands/processingCommandsFromParams.ts new file mode 100644 index 000000000..099846215 --- /dev/null +++ b/packages/base/src/commands/processingCommandsFromParams.ts @@ -0,0 +1,161 @@ +import { + IJGISFormSchemaRegistry, + ProcessingLogicType, + ProcessingType, + ProcessingMerge, + IJGISLayer, + IJGISSource, +} from '@jupytergis/schema'; +import { JupyterFrontEnd } from '@jupyterlab/application'; +import { CommandRegistry } from '@lumino/commands'; +import { UUID } from '@lumino/coreutils'; + +import { getGdal } from '../gdal'; +import { getLayerGeoJSON } from '../processing'; +import { replaceInSql } from '../processing/processingCommands'; +import { JupyterGISTracker } from '../types'; + +/** + * Execute processing directly from params (no UI dialogs). + */ +async function processLayerFromParams( + tracker: JupyterGISTracker, + processingType: ProcessingType, + options: { + sqlQueryFn: (layerName: string, param: any) => string; + gdalFunction: 'ogr2ogr' | 'gdal_rasterize' | 'gdalwarp' | 'gdal_translate'; + gdalOptions: (sqlQuery: string) => string[]; + }, + app: JupyterFrontEnd, + filePath: string, + params: Record, +): Promise { + const current = tracker.find(w => w.model.filePath === filePath); + if (!current) { + return; + } + + const model = current.model; + const { sources = {}, layers = {} } = model.sharedModel; + const inputLayerId = params.inputLayer; + const inputLayer = layers[inputLayerId]; + if (!inputLayer) { + return; + } + + const geojsonString = await getLayerGeoJSON(inputLayer, sources, model); + if (!geojsonString) { + return; + } + + const Gdal = await getGdal(); + const fileBlob = new Blob([geojsonString], { type: 'application/geo+json' }); + const geoFile = new File([fileBlob], 'input.geojson', { + type: 'application/geo+json', + }); + + const result = await Gdal.open(geoFile); + const dataset = result.datasets[0] as any; + const layerName = dataset.info.layers[0].name; + + const sqlQuery = options.sqlQueryFn(layerName, params); + const fullOptions = options.gdalOptions(sqlQuery); + + const outputFilePath = await Gdal.ogr2ogr(dataset, fullOptions); + const processedBytes = await Gdal.getFileBytes(outputFilePath); + Gdal.close(dataset); + + const processedGeoJSON = JSON.parse(new TextDecoder().decode(processedBytes)); + const newSourceId = UUID.uuid4(); + + const sourceModel: IJGISSource = { + type: 'GeoJSONSource', + name: `${processingType} Output`, + parameters: { data: processedGeoJSON }, + }; + + const layerModel: IJGISLayer = { + type: 'VectorLayer', + name: `${processingType} Layer`, + visible: true, + parameters: { source: newSourceId }, + }; + + model.sharedModel.addSource(newSourceId, sourceModel); + model.addLayer(UUID.uuid4(), layerModel); +} + +/** + * Register all processing commands from schema + ProcessingMerge metadata. + */ +export function addProcessingCommandsFromParams(options: { + app: JupyterFrontEnd; + commands: CommandRegistry; + tracker: JupyterGISTracker; + trans: any; + formSchemaRegistry: IJGISFormSchemaRegistry; + processingSchemas: Record; +}): void { + const { app, commands, tracker, trans, processingSchemas } = options; + + for (const proc of ProcessingMerge) { + if (proc.type !== ProcessingLogicType.vector) { + continue; + } + + const schemaKey = Object.keys(processingSchemas).find( + k => k.toLowerCase() === proc.name.toLowerCase(), + ); + const schema = schemaKey ? processingSchemas[schemaKey] : undefined; + if (!schema) { + continue; + } + + const commandId = `${proc.name}WithParams`; + + commands.addCommand(commandId, { + label: trans.__(`${proc.label} from params`), + isEnabled: () => true, + describedBy: { + args: { + type: 'object', + required: ['filePath', 'params'], + properties: { + filePath: { + type: 'string', + description: 'Path to the .jGIS file', + }, + params: schema, + }, + }, + }, + execute: (async (args: { + filePath: string; + params: Record; + }) => { + const { filePath, params } = args; + await processLayerFromParams( + tracker, + proc.name as ProcessingType, + { + sqlQueryFn: (layer, p) => + replaceInSql(proc.operations.sql, p, layer), + gdalFunction: 'ogr2ogr', + gdalOptions: (sql: string) => [ + '-f', + 'GeoJSON', + '-dialect', + 'SQLITE', + '-sql', + sql, + 'output.geojson', + ], + }, + app, + filePath, + params, + ); + }) as any, + }); + } +} diff --git a/packages/base/src/constants.ts b/packages/base/src/constants.ts index 215bf79d4..60fd4e056 100644 --- a/packages/base/src/constants.ts +++ b/packages/base/src/constants.ts @@ -45,15 +45,15 @@ const iconObject = { [CommandIDs.redo]: { icon: redoIcon }, [CommandIDs.undo]: { icon: undoIcon }, [CommandIDs.openLayerBrowser]: { icon: bookOpenIcon }, - [CommandIDs.newRasterEntry]: { icon: rasterIcon }, - [CommandIDs.newVectorTileEntry]: { icon: vectorSquareIcon }, - [CommandIDs.newGeoJSONEntry]: { icon: geoJSONIcon }, - [CommandIDs.newHillshadeEntry]: { icon: moundIcon }, - [CommandIDs.newImageEntry]: { iconClass: 'fa fa-image' }, - [CommandIDs.newVideoEntry]: { iconClass: 'fa fa-video' }, - [CommandIDs.newShapefileEntry]: { iconClass: 'fa fa-file' }, - [CommandIDs.newGeoTiffEntry]: { iconClass: 'fa fa-image' }, - [CommandIDs.newGeoParquetEntry]: { iconClass: 'fa fa-file' }, + [CommandIDs.opeNewRasterDialog]: { icon: rasterIcon }, + [CommandIDs.openNewVectorTileDialog]: { icon: vectorSquareIcon }, + [CommandIDs.openNewGeoJSONDialog]: { icon: geoJSONIcon }, + [CommandIDs.openNewHillshadeDialog]: { icon: moundIcon }, + [CommandIDs.openNewImageDialog]: { iconClass: 'fa fa-image' }, + [CommandIDs.openNewVideoDialog]: { iconClass: 'fa fa-video' }, + [CommandIDs.openNewShapefileDialog]: { iconClass: 'fa fa-file' }, + [CommandIDs.openNewGeoTiffDialog]: { iconClass: 'fa fa-image' }, + [CommandIDs.openNewGeoParquetDialog]: { iconClass: 'fa fa-file' }, [CommandIDs.symbology]: { iconClass: 'fa fa-brush' }, [CommandIDs.identify]: { icon: infoIcon }, [CommandIDs.temporalController]: { icon: clockIcon }, diff --git a/packages/base/src/mainview/mainView.tsx b/packages/base/src/mainview/mainView.tsx index 6da13eb79..edc6c8178 100644 --- a/packages/base/src/mainview/mainView.tsx +++ b/packages/base/src/mainview/mainView.tsx @@ -543,6 +543,16 @@ export class MainView extends React.Component { addContextMenu = (): void => { this._commands.addCommand(CommandIDs.addAnnotation, { + label: 'Add annotation', + describedBy: { + args: { + type: 'object', + properties: {}, + }, + }, + isEnabled: () => { + return !!this._Map; + }, execute: () => { if (!this._Map) { return; @@ -560,10 +570,6 @@ export class MainView extends React.Component { open: true, }); }, - label: 'Add annotation', - isEnabled: () => { - return !!this._Map; - }, }); this._contextMenu.addItem({ diff --git a/packages/base/src/menus.ts b/packages/base/src/menus.ts index 709552402..af11b070e 100644 --- a/packages/base/src/menus.ts +++ b/packages/base/src/menus.ts @@ -13,22 +13,22 @@ export const vectorSubMenu = (commands: CommandRegistry) => { subMenu.addItem({ type: 'command', - command: CommandIDs.newVectorTileEntry, + command: CommandIDs.openNewVectorTileDialog, }); subMenu.addItem({ type: 'command', - command: CommandIDs.newGeoJSONEntry, + command: CommandIDs.openNewGeoJSONDialog, }); subMenu.addItem({ type: 'command', - command: CommandIDs.newShapefileEntry, + command: CommandIDs.openNewShapefileDialog, }); subMenu.addItem({ type: 'command', - command: CommandIDs.newGeoParquetEntry, + command: CommandIDs.openNewGeoParquetDialog, }); return subMenu; @@ -43,22 +43,22 @@ export const rasterSubMenu = (commands: CommandRegistry) => { subMenu.addItem({ type: 'command', - command: CommandIDs.newRasterEntry, + command: CommandIDs.opeNewRasterDialog, }); subMenu.addItem({ type: 'command', - command: CommandIDs.newHillshadeEntry, + command: CommandIDs.openNewHillshadeDialog, }); subMenu.addItem({ type: 'command', - command: CommandIDs.newImageEntry, + command: CommandIDs.openNewImageDialog, }); subMenu.addItem({ type: 'command', - command: CommandIDs.newGeoTiffEntry, + command: CommandIDs.openNewGeoTiffDialog, }); return subMenu; diff --git a/packages/base/src/panelview/components/layers.tsx b/packages/base/src/panelview/components/layers.tsx index cdb1873c6..da3ad7362 100644 --- a/packages/base/src/panelview/components/layers.tsx +++ b/packages/base/src/panelview/components/layers.tsx @@ -150,7 +150,7 @@ export const LayersBodyComponent: React.FC = props => { } else { // Check if new selection is the same type as previous selections const isSelectedSameType = Object.values(selectedValue).some( - selection => (selection as ISelection).type === type, + selection => selection.type === type, ); if (!isSelectedSameType) { diff --git a/packages/base/src/processing/processingCommands.ts b/packages/base/src/processing/processingCommands.ts index a5b613928..227c5f075 100644 --- a/packages/base/src/processing/processingCommands.ts +++ b/packages/base/src/processing/processingCommands.ts @@ -51,6 +51,12 @@ export function addProcessingCommands( if (processingElement.type === ProcessingLogicType.vector) { commands.addCommand(processingElement.name, { label: trans.__(processingElement.label), + describedBy: { + args: { + type: 'object', + properties: {}, + }, + }, isEnabled: () => selectedLayerIsOfType(['VectorLayer'], tracker), execute: async () => { await processSelectedLayer( diff --git a/python/jupytergis_core/src/jgisplugin/plugins.ts b/python/jupytergis_core/src/jgisplugin/plugins.ts index f1139b060..c27d20ecf 100644 --- a/python/jupytergis_core/src/jgisplugin/plugins.ts +++ b/python/jupytergis_core/src/jgisplugin/plugins.ts @@ -161,6 +161,22 @@ const activate = async ( app.commands.addCommand(CommandIDs.createNew, { label: args => (args['label'] as string) ?? 'GIS Project', + describedBy: { + args: { + type: 'object', + properties: { + label: { + type: 'string', + description: 'The label for the file creation command', + }, + cwd: { + type: 'string', + description: + 'The current working directory where the file should be created', + }, + }, + }, + }, caption: 'Create a new JGIS Editor', icon: args => logoIcon, execute: async args => { @@ -215,22 +231,22 @@ const activate = async ( // Layers and Sources palette.addItem({ - command: CommandIDs.newRasterEntry, + command: CommandIDs.opeNewRasterDialog, category: 'JupyterGIS', }); palette.addItem({ - command: CommandIDs.newVectorTileEntry, + command: CommandIDs.openNewVectorTileDialog, category: 'JupyterGIS', }); palette.addItem({ - command: CommandIDs.newGeoJSONEntry, + command: CommandIDs.openNewGeoJSONDialog, category: 'JupyterGIS', }); palette.addItem({ - command: CommandIDs.newHillshadeEntry, + command: CommandIDs.openNewHillshadeDialog, category: 'JupyterGIS', }); diff --git a/python/jupytergis_qgis/src/plugins.ts b/python/jupytergis_qgis/src/plugins.ts index 21e5141ce..b27c80c41 100644 --- a/python/jupytergis_qgis/src/plugins.ts +++ b/python/jupytergis_qgis/src/plugins.ts @@ -214,6 +214,18 @@ const activate = async ( if (installed) { app.commands.addCommand(CommandIDs.exportQgis, { label: 'Export To QGZ', + describedBy: { + args: { + type: 'object', + properties: { + filepath: { + type: 'string', + description: + 'Optional. Destination filename (with or without .qgz extension) for the exported QGIS project.', + }, + }, + }, + }, isEnabled: () => tracker.currentWidget ? tracker.currentWidget.model.sharedModel.editable