From 67e2d36049f493f3ca5d3dfa2f517a4c3060e3ad Mon Sep 17 00:00:00 2001 From: Matt Fisher Date: Wed, 2 Apr 2025 10:14:07 -0600 Subject: [PATCH 1/3] Init "Save as..." command and toolbar button Co-authored-by: Arjun Verma Co-authored-by: Martin Renou Co-authored-by: Yao-Ting Yao Co-authored-by: Jamison Polackwich <7376361+rjpolackwich@users.noreply.github.com> Co-authored-by: Tammy Woodard <19804979+tawoodard@users.noreply.github.com> --- packages/base/src/commands.ts | 41 ++++++++- packages/base/src/constants.ts | 4 +- .../objectform/bufferProcessForm.tsx | 92 +++++++++++++++++++ packages/base/src/toolbar/widget.tsx | 8 ++ 4 files changed, 143 insertions(+), 2 deletions(-) create mode 100644 packages/base/src/formbuilder/objectform/bufferProcessForm.tsx diff --git a/packages/base/src/commands.ts b/packages/base/src/commands.ts index a9198e989..c01b1b797 100644 --- a/packages/base/src/commands.ts +++ b/packages/base/src/commands.ts @@ -10,7 +10,7 @@ import { SourceType } from '@jupytergis/schema'; import { JupyterFrontEnd } from '@jupyterlab/application'; -import { showErrorMessage } from '@jupyterlab/apputils'; +import { InputDialog, showErrorMessage } from '@jupyterlab/apputils'; import { ICompletionProviderManager } from '@jupyterlab/completer'; import { IStateDB } from '@jupyterlab/statedb'; import { ITranslator } from '@jupyterlab/translation'; @@ -252,6 +252,45 @@ export function addCommands( ...icons.get(CommandIDs.temporalController) }); + commands.addCommand(CommandIDs.saveAs, { + label: trans.__('Save As...'), + isEnabled: () => true, + execute: async () => { + const oldFilename = tracker.currentWidget?.model.filePath; + const newFilename = (await InputDialog.getText({ + title: 'Save as...', + label: 'New filename', + placeholder: oldFilename, + })).value; + + if (!newFilename) { + return; + } + + const content = tracker.currentWidget?.model.toJSON(); + + // FIXME: This doesn't re-open the project file in the current view where the save button was clicked. + app.serviceManager.contents.save(newFilename, { + content: JSON.stringify(content), + format: 'text', + type: 'file', + mimetype: 'text/json' + }); + // FIXME: Saves to the currently open directory, while the above save is to the JupyterLab root directory. + // FIXME: Get "unsaved_project" from a constant + // FIXME: unsaved_project is getting saved even though we're trying not to! + if (oldFilename && oldFilename.endsWith('unsaved_project')) { + app.serviceManager.contents.save(oldFilename, { + content: JSON.stringify(content), + format: 'text', + type: 'file', + mimetype: 'text/json' + }); + } + }, + ...icons.get(CommandIDs.saveAs), + }) + /** * SOURCES and LAYERS creation commands. */ diff --git a/packages/base/src/constants.ts b/packages/base/src/constants.ts index eb91c975d..4acbc909a 100644 --- a/packages/base/src/constants.ts +++ b/packages/base/src/constants.ts @@ -11,6 +11,7 @@ export namespace CommandIDs { export const symbology = 'jupytergis:symbology'; export const identify = 'jupytergis:identify'; export const temporalController = 'jupytergis:temporalController'; + export const saveAs = 'jupytergis:saveAs'; // Layers and sources creation commands export const openLayerBrowser = 'jupytergis:openLayerBrowser'; @@ -106,7 +107,8 @@ const iconObject = { [CommandIDs.newGeoTiffEntry]: { iconClass: 'fa fa-image' }, [CommandIDs.symbology]: { iconClass: 'fa fa-brush' }, [CommandIDs.identify]: { iconClass: 'fa fa-info' }, - [CommandIDs.temporalController]: { iconClass: 'fa fa-clock' } + [CommandIDs.temporalController]: { iconClass: 'fa fa-clock' }, + [CommandIDs.saveAs]: {iconClass: 'fa fa-save'} }; /** diff --git a/packages/base/src/formbuilder/objectform/bufferProcessForm.tsx b/packages/base/src/formbuilder/objectform/bufferProcessForm.tsx new file mode 100644 index 000000000..87eb5cbe2 --- /dev/null +++ b/packages/base/src/formbuilder/objectform/bufferProcessForm.tsx @@ -0,0 +1,92 @@ +import { BaseForm, IBaseFormProps, IBaseFormStates } from './baseform'; // Ensure BaseForm imports states +import { IDict, IJupyterGISModel } from '@jupytergis/schema'; +import { IChangeEvent } from '@rjsf/core'; +// import { loadFile } from '../../tools'; +import proj4 from 'proj4'; + +interface IBufferFormOptions extends IBaseFormProps { + schema: IDict; + sourceData: IDict; + title: string; + cancelButton: (() => void) | boolean; + syncData: (props: IDict) => void; + model: IJupyterGISModel; +} + +export class BufferForm extends BaseForm { + private model: IJupyterGISModel; + private unit = ''; + + constructor(options: IBufferFormOptions) { + super(options); + this.model = options.model; + + // Ensure initial state matches IBaseFormStates + this.state = { + schema: options.schema ?? {} // Ensure schema is never undefined + }; + + this.onFormChange = this.handleFormChange.bind(this); + + this.computeDistanceUnits(options.sourceData.inputLayer); + } + + private async computeDistanceUnits(layerId: string) { + const layer = this.model.getLayer(layerId); + if (!layer?.parameters?.source) { + return; + } + const source = this.model.getSource(layer.parameters.source); + if (!source) { + return; + } + + const projection = source.parameters?.projection; + console.log(projection); + + // TODO: how to get layer info from OpenLayers? + // const srs = layer.from_ol().srs; + const srs = 'EPSG:4326'; + + try { + // console.log(proj4, srs); + this.unit = (proj4(srs) as any).oProj.units; + debugger; + this.updateSchema(); + } catch (error) { + console.error('Error calculating units:', error); + } + } + + public handleFormChange(e: IChangeEvent) { + super.onFormChange(e); + + if (e.formData.inputLayer) { + this.computeDistanceUnits(e.formData.inputLayer); + } + } + + private updateSchema() { + this.setState( + (prevState: IBaseFormStates) => ({ + schema: { + ...prevState.schema, + properties: { + ...prevState.schema?.properties, + bufferDistance: { + ...prevState.schema?.properties?.bufferDistance, + description: + prevState.schema?.properties?.bufferDistance.description.replace( + 'projection units', + this.unit + ) + } + } + } + }), + () => { + this.forceUpdate(); + } + ); + } +} diff --git a/packages/base/src/toolbar/widget.tsx b/packages/base/src/toolbar/widget.tsx index ce19e8c53..552a0bf81 100644 --- a/packages/base/src/toolbar/widget.tsx +++ b/packages/base/src/toolbar/widget.tsx @@ -39,6 +39,14 @@ export class ToolbarWidget extends ReactiveToolbar { this.addClass('jGIS-toolbar-widget'); if (options.commands) { + this.addItem( + 'Save as...', + new CommandToolbarButton({ + id: CommandIDs.saveAs, + label: '', + commands: options.commands + }), + ); this.addItem( 'undo', new CommandToolbarButton({ From cc33b30f73434bb3d46932d10c0e5d0238c1c3b2 Mon Sep 17 00:00:00 2001 From: Matt Fisher Date: Mon, 7 Apr 2025 12:33:22 -0600 Subject: [PATCH 2/3] Don't save "unsaved_project" files Co-authored-by: Arjun Verma --- packages/base/src/commands.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/base/src/commands.ts b/packages/base/src/commands.ts index c01b1b797..ddd2cb244 100644 --- a/packages/base/src/commands.ts +++ b/packages/base/src/commands.ts @@ -279,7 +279,7 @@ export function addCommands( // FIXME: Saves to the currently open directory, while the above save is to the JupyterLab root directory. // FIXME: Get "unsaved_project" from a constant // FIXME: unsaved_project is getting saved even though we're trying not to! - if (oldFilename && oldFilename.endsWith('unsaved_project')) { + if (oldFilename && !oldFilename.endsWith('unsaved_project')) { app.serviceManager.contents.save(oldFilename, { content: JSON.stringify(content), format: 'text', From 4f2dce7c991b1a235ced2e1ad72b29e549e2c851 Mon Sep 17 00:00:00 2001 From: Matt Fisher Date: Mon, 7 Apr 2025 12:34:23 -0600 Subject: [PATCH 3/3] Update model filePath when saving Fix the file extension automatically if it doesn't end in jGIS FIXME: Does not save changes to the shared document after changing the filePath. Co-authored-by: Arjun Verma --- packages/base/src/commands.ts | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/packages/base/src/commands.ts b/packages/base/src/commands.ts index ddd2cb244..c8870607e 100644 --- a/packages/base/src/commands.ts +++ b/packages/base/src/commands.ts @@ -256,18 +256,31 @@ export function addCommands( label: trans.__('Save As...'), isEnabled: () => true, execute: async () => { - const oldFilename = tracker.currentWidget?.model.filePath; - const newFilename = (await InputDialog.getText({ - title: 'Save as...', - label: 'New filename', - placeholder: oldFilename, - })).value; + if (!tracker.currentWidget) { + return; + } + + const model = tracker.currentWidget.model; + const oldFilename = model.filePath; + let newFilename = ( + await InputDialog.getText({ + title: 'Save as...', + label: 'New filename', + placeholder: oldFilename + }) + ).value; if (!newFilename) { return; } - const content = tracker.currentWidget?.model.toJSON(); + if (newFilename.toLowerCase().endsWith('.qgz')) { + throw Error('Not supported yet'); + } else if (!newFilename.toLowerCase().endsWith('.jgis')) { + newFilename += '.jGIS'; + } + + const content = model.toJSON(); // FIXME: This doesn't re-open the project file in the current view where the save button was clicked. app.serviceManager.contents.save(newFilename, { @@ -276,9 +289,11 @@ export function addCommands( type: 'file', mimetype: 'text/json' }); + // FIXME: The widget will only save to this new filename once, as opposed to continuously saving changes to the file like we expect. + model.filePath = newFilename; + // FIXME: Saves to the currently open directory, while the above save is to the JupyterLab root directory. // FIXME: Get "unsaved_project" from a constant - // FIXME: unsaved_project is getting saved even though we're trying not to! if (oldFilename && !oldFilename.endsWith('unsaved_project')) { app.serviceManager.contents.save(oldFilename, { content: JSON.stringify(content),