@jupytergis/base 0.13.2 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/lib/commands/BaseCommandIDs.d.ts +14 -13
  2. package/lib/commands/BaseCommandIDs.js +14 -14
  3. package/lib/commands/index.js +528 -130
  4. package/lib/commands/operationCommands.d.ts +22 -0
  5. package/lib/commands/operationCommands.js +305 -0
  6. package/lib/constants.js +11 -9
  7. package/lib/dialogs/ProcessingFormDialog.d.ts +1 -1
  8. package/lib/dialogs/ProcessingFormDialog.js +2 -2
  9. package/lib/dialogs/layerBrowserDialog.d.ts +2 -0
  10. package/lib/dialogs/layerBrowserDialog.js +12 -5
  11. package/lib/dialogs/layerCreationFormDialog.d.ts +7 -0
  12. package/lib/dialogs/layerCreationFormDialog.js +10 -2
  13. package/lib/dialogs/symbology/components/color_ramp/ColorRampControls.d.ts +2 -1
  14. package/lib/dialogs/symbology/components/color_ramp/ColorRampControls.js +21 -19
  15. package/lib/dialogs/symbology/components/color_ramp/ColorRampSelector.d.ts +3 -1
  16. package/lib/dialogs/symbology/components/color_ramp/ColorRampSelector.js +13 -5
  17. package/lib/dialogs/symbology/tiff_layer/types/SingleBandPseudoColor.js +6 -4
  18. package/lib/dialogs/symbology/vector_layer/types/Categorized.js +7 -11
  19. package/lib/dialogs/symbology/vector_layer/types/Graduated.js +7 -10
  20. package/lib/dialogs/symbology/vector_layer/types/Heatmap.js +7 -8
  21. package/lib/formbuilder/creationform.d.ts +8 -8
  22. package/lib/formbuilder/creationform.js +130 -85
  23. package/lib/formbuilder/editform.d.ts +1 -7
  24. package/lib/formbuilder/editform.js +64 -52
  25. package/lib/formbuilder/formselectors.d.ts +5 -4
  26. package/lib/formbuilder/index.d.ts +1 -1
  27. package/lib/formbuilder/index.js +1 -1
  28. package/lib/formbuilder/objectform/SchemaForm.d.ts +36 -0
  29. package/lib/formbuilder/objectform/SchemaForm.js +77 -0
  30. package/lib/formbuilder/objectform/StoryEditorForm.d.ts +3 -9
  31. package/lib/formbuilder/objectform/StoryEditorForm.js +20 -14
  32. package/lib/formbuilder/objectform/components/LayerSelect.js +3 -8
  33. package/lib/formbuilder/objectform/components/SegmentFormSymbology.js +23 -10
  34. package/lib/formbuilder/objectform/components/SourcePropertiesField.d.ts +7 -0
  35. package/lib/formbuilder/objectform/components/SourcePropertiesField.js +29 -0
  36. package/lib/formbuilder/objectform/components/StorySegmentReset.js +1 -1
  37. package/lib/formbuilder/objectform/fileselectorwidget.js +1 -1
  38. package/lib/formbuilder/objectform/layer/heatmapLayerForm.d.ts +3 -12
  39. package/lib/formbuilder/objectform/layer/heatmapLayerForm.js +87 -55
  40. package/lib/formbuilder/objectform/layer/hillshadeLayerForm.d.ts +3 -8
  41. package/lib/formbuilder/objectform/layer/hillshadeLayerForm.js +36 -10
  42. package/lib/formbuilder/objectform/layer/layerform.d.ts +7 -9
  43. package/lib/formbuilder/objectform/layer/layerform.js +33 -20
  44. package/lib/formbuilder/objectform/layer/storySegmentLayerForm.d.ts +3 -5
  45. package/lib/formbuilder/objectform/layer/storySegmentLayerForm.js +73 -29
  46. package/lib/formbuilder/objectform/layer/vectorlayerform.d.ts +3 -14
  47. package/lib/formbuilder/objectform/layer/vectorlayerform.js +36 -29
  48. package/lib/formbuilder/objectform/layer/webGlLayerForm.d.ts +3 -10
  49. package/lib/formbuilder/objectform/layer/webGlLayerForm.js +37 -13
  50. package/lib/formbuilder/objectform/process/dissolveProcessForm.d.ts +8 -18
  51. package/lib/formbuilder/objectform/process/dissolveProcessForm.js +68 -56
  52. package/lib/formbuilder/objectform/processingForm.d.ts +12 -0
  53. package/lib/formbuilder/objectform/processingForm.js +33 -0
  54. package/lib/formbuilder/objectform/schemaUtils.d.ts +16 -0
  55. package/lib/formbuilder/objectform/schemaUtils.js +59 -0
  56. package/lib/formbuilder/objectform/source/geojsonsource.d.ts +3 -19
  57. package/lib/formbuilder/objectform/source/geojsonsource.js +94 -53
  58. package/lib/formbuilder/objectform/source/geotiffsource.d.ts +3 -20
  59. package/lib/formbuilder/objectform/source/geotiffsource.js +73 -74
  60. package/lib/formbuilder/objectform/source/pathbasedsource.d.ts +3 -19
  61. package/lib/formbuilder/objectform/source/pathbasedsource.js +76 -75
  62. package/lib/formbuilder/objectform/source/sourceform.d.ts +7 -8
  63. package/lib/formbuilder/objectform/source/sourceform.js +28 -11
  64. package/lib/formbuilder/objectform/source/tilesourceform.d.ts +3 -7
  65. package/lib/formbuilder/objectform/source/tilesourceform.js +63 -53
  66. package/lib/formbuilder/objectform/useSchemaFormState.d.ts +48 -0
  67. package/lib/formbuilder/objectform/useSchemaFormState.js +35 -0
  68. package/lib/index.d.ts +0 -1
  69. package/lib/index.js +0 -1
  70. package/lib/keybindings.json +10 -10
  71. package/lib/mainview/mainView.d.ts +11 -7
  72. package/lib/mainview/mainView.js +63 -36
  73. package/lib/mainview/mainviewmodel.js +2 -2
  74. package/lib/menus.js +8 -8
  75. package/lib/panelview/components/layers.js +5 -2
  76. package/lib/panelview/objectproperties.js +2 -2
  77. package/lib/panelview/rightpanel.js +17 -2
  78. package/lib/panelview/story-maps/SpectaPanel.d.ts +15 -0
  79. package/lib/panelview/story-maps/SpectaPanel.js +35 -0
  80. package/lib/panelview/story-maps/StoryViewerPanel.d.ts +24 -9
  81. package/lib/panelview/story-maps/StoryViewerPanel.js +22 -268
  82. package/lib/panelview/story-maps/{PreviewModeSwitch.js → components/PreviewModeSwitch.js} +1 -1
  83. package/lib/panelview/story-maps/components/SpectaDesktopView.d.ts +21 -0
  84. package/lib/panelview/story-maps/components/SpectaDesktopView.js +49 -0
  85. package/lib/panelview/story-maps/components/SpectaMobileView.d.ts +17 -0
  86. package/lib/panelview/story-maps/{MobileSpectaPanel.js → components/SpectaMobileView.js} +8 -22
  87. package/lib/panelview/story-maps/{StoryNavBar.d.ts → components/StoryNavBar.d.ts} +1 -1
  88. package/lib/panelview/story-maps/{StoryNavBar.js → components/StoryNavBar.js} +2 -4
  89. package/lib/panelview/story-maps/hooks/useStoryMap.d.ts +39 -0
  90. package/lib/panelview/story-maps/hooks/useStoryMap.js +252 -0
  91. package/lib/processing/index.d.ts +2 -2
  92. package/lib/processing/index.js +62 -35
  93. package/lib/processing/processingCommands.d.ts +1 -1
  94. package/lib/processing/processingCommands.js +26 -6
  95. package/lib/shared/components/Collapsible.d.ts +6 -0
  96. package/lib/shared/components/Collapsible.js +26 -0
  97. package/lib/statusbar/SpectaPresentationProgressBar.d.ts +7 -0
  98. package/lib/statusbar/SpectaPresentationProgressBar.js +40 -0
  99. package/lib/toolbar/widget.js +4 -2
  100. package/lib/tools.d.ts +6 -0
  101. package/lib/tools.js +9 -0
  102. package/lib/types.d.ts +29 -2
  103. package/lib/widget.js +14 -2
  104. package/package.json +2 -4
  105. package/style/base.css +23 -1
  106. package/style/dialog.css +5 -0
  107. package/style/leftPanel.css +14 -37
  108. package/style/shared/button.css +0 -10
  109. package/style/shared/switch.css +8 -7
  110. package/style/spectaProgressBar.css +144 -0
  111. package/style/storyPanel.css +33 -0
  112. package/style/symbologyDialog.css +2 -2
  113. package/lib/formbuilder/objectform/baseform.d.ts +0 -91
  114. package/lib/formbuilder/objectform/baseform.js +0 -231
  115. package/lib/panelview/story-maps/MobileSpectaPanel.d.ts +0 -7
  116. /package/lib/panelview/story-maps/{PreviewModeSwitch.d.ts → components/PreviewModeSwitch.d.ts} +0 -0
@@ -1,4 +1,4 @@
1
- import { showErrorMessage } from '@jupyterlab/apputils';
1
+ import { UUID } from '@lumino/coreutils';
2
2
  import { fromLonLat } from 'ol/proj';
3
3
  import { CommandIDs, icons } from '../constants';
4
4
  import { ProcessingFormDialog } from '../dialogs/ProcessingFormDialog';
@@ -12,6 +12,7 @@ import { addProcessingCommands } from '../processing/processingCommands';
12
12
  import { getGeoJSONDataFromLayerSource, downloadFile } from '../tools';
13
13
  import { SYMBOLOGY_VALID_LAYER_TYPES } from '../types';
14
14
  import { JupyterGISDocumentWidget } from '../widget';
15
+ import { addLayerCreationCommands } from './operationCommands';
15
16
  const POINT_SELECTION_TOOL_CLASS = 'jGIS-point-selection-tool';
16
17
  function loadKeybindings(commands, keybindings) {
17
18
  keybindings.forEach(binding => {
@@ -29,6 +30,7 @@ export function addCommands(app, tracker, translator, formSchemaRegistry, layerB
29
30
  var _a;
30
31
  const trans = translator.load('jupyterlab');
31
32
  const { commands } = app;
33
+ addLayerCreationCommands({ tracker, commands, trans });
32
34
  /**
33
35
  * Wraps a command definition to automatically disable it in Specta mode
34
36
  */
@@ -54,7 +56,17 @@ export function addCommands(app, tracker, translator, formSchemaRegistry, layerB
54
56
  commands.addCommand = (id, options) => {
55
57
  return originalAddCommand(id, createSpectaAwareCommand(options));
56
58
  };
57
- commands.addCommand(CommandIDs.symbology, Object.assign({ label: trans.__('Edit Symbology'), isEnabled: () => {
59
+ commands.addCommand(CommandIDs.symbology, Object.assign({ label: trans.__('Edit Symbology'), caption: 'Open the symbology editor for the currently selected layer.', describedBy: {
60
+ args: {
61
+ type: 'object',
62
+ properties: {
63
+ selected: {
64
+ type: 'object',
65
+ description: 'Currently selected layer(s) in the map view',
66
+ },
67
+ },
68
+ },
69
+ }, isEnabled: () => {
58
70
  var _a, _b;
59
71
  const model = (_a = tracker.currentWidget) === null || _a === void 0 ? void 0 : _a.model;
60
72
  const localState = model === null || model === void 0 ? void 0 : model.sharedModel.awareness.getLocalState();
@@ -74,27 +86,65 @@ export function addCommands(app, tracker, translator, formSchemaRegistry, layerB
74
86
  const isValidLayer = SYMBOLOGY_VALID_LAYER_TYPES.includes(layer.type);
75
87
  return isValidLayer;
76
88
  }, execute: Private.createSymbologyDialog(tracker, state) }, icons.get(CommandIDs.symbology)));
77
- commands.addCommand(CommandIDs.redo, Object.assign({ label: trans.__('Redo'), isEnabled: () => {
89
+ commands.addCommand(CommandIDs.redo, Object.assign({ label: trans.__('Redo'), caption: 'Redo the last undone operation in the specified JupyterGIS document. If filePath is omitted, use the active document.', describedBy: {
90
+ args: {
91
+ type: 'object',
92
+ properties: {
93
+ filePath: {
94
+ type: 'string',
95
+ description: 'Optional .jGIS file path. If omitted, uses active widget.',
96
+ },
97
+ },
98
+ },
99
+ }, isEnabled: () => {
78
100
  return tracker.currentWidget
79
101
  ? tracker.currentWidget.model.sharedModel.editable
80
102
  : false;
81
- }, execute: () => {
82
- const current = tracker.currentWidget;
103
+ }, execute: (args) => {
104
+ const filePath = args === null || args === void 0 ? void 0 : args.filePath;
105
+ const current = filePath
106
+ ? tracker.find(w => w.model.filePath === filePath)
107
+ : tracker.currentWidget;
83
108
  if (current) {
84
109
  return current.model.sharedModel.redo();
85
110
  }
86
111
  } }, (_a = icons.get(CommandIDs.redo)) === null || _a === void 0 ? void 0 : _a.icon));
87
- commands.addCommand(CommandIDs.undo, Object.assign({ label: trans.__('Undo'), isEnabled: () => {
112
+ commands.addCommand(CommandIDs.undo, Object.assign({ label: trans.__('Undo'), caption: 'Undo the last operation in the specified JupyterGIS document. If filePath is omitted, use the active document.', describedBy: {
113
+ args: {
114
+ type: 'object',
115
+ required: [],
116
+ properties: {
117
+ filePath: {
118
+ type: 'string',
119
+ description: 'Optional .jGIS file path. If omitted, uses active widget.',
120
+ },
121
+ },
122
+ },
123
+ }, isEnabled: () => {
88
124
  return tracker.currentWidget
89
125
  ? tracker.currentWidget.model.sharedModel.editable
90
126
  : false;
91
- }, execute: () => {
92
- const current = tracker.currentWidget;
127
+ }, execute: (args) => {
128
+ const filePath = args === null || args === void 0 ? void 0 : args.filePath;
129
+ const current = filePath
130
+ ? tracker.find(w => w.model.filePath === filePath)
131
+ : tracker.currentWidget;
93
132
  if (current) {
94
133
  return current.model.sharedModel.undo();
95
134
  }
96
135
  } }, icons.get(CommandIDs.undo)));
97
- commands.addCommand(CommandIDs.identify, Object.assign({ label: trans.__('Identify'), isToggled: () => {
136
+ commands.addCommand(CommandIDs.identify, Object.assign({ label: trans.__('Identify'), caption: 'Toggle identify mode for the selected vector-compatible layer in the specified JupyterGIS document.', describedBy: {
137
+ args: {
138
+ type: 'object',
139
+ required: [],
140
+ properties: {
141
+ filePath: {
142
+ type: 'string',
143
+ description: 'Optional .jGIS file path. If omitted, uses active widget.',
144
+ },
145
+ },
146
+ },
147
+ }, isToggled: () => {
98
148
  const current = tracker.currentWidget;
99
149
  if (!current) {
100
150
  return false;
@@ -131,7 +181,10 @@ export function addCommands(app, tracker, translator, formSchemaRegistry, layerB
131
181
  'VectorTileLayer',
132
182
  ].includes(selectedLayer.type);
133
183
  }, execute: args => {
134
- const current = tracker.currentWidget;
184
+ const filePath = args === null || args === void 0 ? void 0 : args.filePath;
185
+ const current = filePath
186
+ ? tracker.find(w => w.model.filePath === filePath)
187
+ : tracker.currentWidget;
135
188
  if (!current) {
136
189
  return;
137
190
  }
@@ -149,7 +202,17 @@ export function addCommands(app, tracker, translator, formSchemaRegistry, layerB
149
202
  current.model.toggleMode('identifying');
150
203
  commands.notifyCommandChanged(CommandIDs.identify);
151
204
  } }, icons.get(CommandIDs.identify)));
152
- commands.addCommand(CommandIDs.temporalController, Object.assign({ label: trans.__('Temporal Controller'), isToggled: () => {
205
+ commands.addCommand(CommandIDs.temporalController, Object.assign({ label: trans.__('Temporal Controller'), caption: 'Toggle the temporal controller for the selected vector or heatmap layer in the specified JupyterGIS document.', describedBy: {
206
+ args: {
207
+ type: 'object',
208
+ properties: {
209
+ filePath: {
210
+ type: 'string',
211
+ description: 'Optional path to the .jGIS file',
212
+ },
213
+ },
214
+ },
215
+ }, isToggled: () => {
153
216
  var _a;
154
217
  return ((_a = tracker.currentWidget) === null || _a === void 0 ? void 0 : _a.model.isTemporalControllerActive) || false;
155
218
  }, isEnabled: () => {
@@ -177,8 +240,11 @@ export function addCommands(app, tracker, translator, formSchemaRegistry, layerB
177
240
  return false;
178
241
  }
179
242
  return true;
180
- }, execute: () => {
181
- const current = tracker.currentWidget;
243
+ }, execute: (args) => {
244
+ const filePath = args === null || args === void 0 ? void 0 : args.filePath;
245
+ const current = filePath
246
+ ? tracker.find(w => w.model.filePath === filePath)
247
+ : tracker.currentWidget;
182
248
  if (!current) {
183
249
  return;
184
250
  }
@@ -188,7 +254,12 @@ export function addCommands(app, tracker, translator, formSchemaRegistry, layerB
188
254
  /**
189
255
  * SOURCES and LAYERS creation commands.
190
256
  */
191
- commands.addCommand(CommandIDs.openLayerBrowser, Object.assign({ label: trans.__('Open Layer Browser'), isEnabled: () => {
257
+ commands.addCommand(CommandIDs.openLayerBrowser, Object.assign({ label: trans.__('Open Layer Browser'), caption: 'Open the layer browser dialog to browse and add available layers to the current JupyterGIS document.', describedBy: {
258
+ args: {
259
+ type: 'object',
260
+ properties: {},
261
+ },
262
+ }, isEnabled: () => {
192
263
  return tracker.currentWidget
193
264
  ? tracker.currentWidget.model.sharedModel.editable
194
265
  : false;
@@ -196,7 +267,12 @@ export function addCommands(app, tracker, translator, formSchemaRegistry, layerB
196
267
  /**
197
268
  * Source and layers
198
269
  */
199
- commands.addCommand(CommandIDs.newRasterEntry, Object.assign({ label: trans.__('New Raster Tile Layer'), isEnabled: () => {
270
+ commands.addCommand(CommandIDs.openNewRasterDialog, Object.assign({ label: trans.__('Open New Raster Tile Layer Creation Dialog'), caption: 'Open a dialog to create a new raster tile layer and source in the current JupyterGIS document.', describedBy: {
271
+ args: {
272
+ type: 'object',
273
+ properties: {},
274
+ },
275
+ }, isEnabled: () => {
200
276
  return tracker.currentWidget
201
277
  ? tracker.currentWidget.model.sharedModel.editable
202
278
  : false;
@@ -206,15 +282,16 @@ export function addCommands(app, tracker, translator, formSchemaRegistry, layerB
206
282
  title: 'Create Raster Tile Layer',
207
283
  createLayer: true,
208
284
  createSource: true,
209
- sourceData: {
210
- minZoom: 0,
211
- maxZoom: 24,
212
- },
213
285
  layerData: { name: 'Custom Raster Tile Layer' },
214
286
  sourceType: 'RasterSource',
215
287
  layerType: 'RasterLayer',
216
- }) }, icons.get(CommandIDs.newRasterEntry)));
217
- commands.addCommand(CommandIDs.newVectorTileEntry, Object.assign({ label: trans.__('New Vector Tile Layer'), isEnabled: () => {
288
+ }) }, icons.get(CommandIDs.openNewRasterDialog)));
289
+ commands.addCommand(CommandIDs.openNewVectorTileDialog, Object.assign({ label: trans.__('Open New Vector Tile Layer Creation Dialog'), caption: 'Open a dialog to create a new vector tile layer and source in the current JupyterGIS document.', describedBy: {
290
+ args: {
291
+ type: 'object',
292
+ properties: {},
293
+ },
294
+ }, isEnabled: () => {
218
295
  return tracker.currentWidget
219
296
  ? tracker.currentWidget.model.sharedModel.editable
220
297
  : false;
@@ -224,12 +301,16 @@ export function addCommands(app, tracker, translator, formSchemaRegistry, layerB
224
301
  title: 'Create Vector Tile Layer',
225
302
  createLayer: true,
226
303
  createSource: true,
227
- sourceData: { minZoom: 0, maxZoom: 24 },
228
304
  layerData: { name: 'Custom Vector Tile Layer' },
229
305
  sourceType: 'VectorTileSource',
230
306
  layerType: 'VectorTileLayer',
231
- }) }, icons.get(CommandIDs.newVectorTileEntry)));
232
- commands.addCommand(CommandIDs.newGeoParquetEntry, Object.assign({ label: trans.__('New GeoParquet Layer'), isEnabled: () => {
307
+ }) }, icons.get(CommandIDs.openNewVectorTileDialog)));
308
+ commands.addCommand(CommandIDs.openNewGeoParquetDialog, Object.assign({ label: trans.__('Open New GeoParquet Layer Creation Dialog'), caption: 'Open a dialog to create a new GeoParquet layer and source in the current JupyterGIS document.', describedBy: {
309
+ args: {
310
+ type: 'object',
311
+ properties: {},
312
+ },
313
+ }, isEnabled: () => {
233
314
  return tracker.currentWidget
234
315
  ? tracker.currentWidget.model.sharedModel.editable
235
316
  : false;
@@ -243,8 +324,13 @@ export function addCommands(app, tracker, translator, formSchemaRegistry, layerB
243
324
  layerData: { name: 'Custom GeoParquet Layer' },
244
325
  sourceType: 'GeoParquetSource',
245
326
  layerType: 'VectorLayer',
246
- }) }, icons.get(CommandIDs.newGeoParquetEntry)));
247
- commands.addCommand(CommandIDs.newGeoJSONEntry, Object.assign({ label: trans.__('New GeoJSON layer'), isEnabled: () => {
327
+ }) }, icons.get(CommandIDs.openNewGeoParquetDialog)));
328
+ commands.addCommand(CommandIDs.openNewGeoJSONDialog, Object.assign({ label: trans.__('Open New GeoJSON Layer Creation Dialog'), caption: 'Open a dialog to create a new GeoJSON layer and source in the current JupyterGIS document.', describedBy: {
329
+ args: {
330
+ type: 'object',
331
+ properties: {},
332
+ },
333
+ }, isEnabled: () => {
248
334
  return tracker.currentWidget
249
335
  ? tracker.currentWidget.model.sharedModel.editable
250
336
  : false;
@@ -257,10 +343,15 @@ export function addCommands(app, tracker, translator, formSchemaRegistry, layerB
257
343
  layerData: { name: 'Custom GeoJSON Layer' },
258
344
  sourceType: 'GeoJSONSource',
259
345
  layerType: 'VectorLayer',
260
- }) }, icons.get(CommandIDs.newGeoJSONEntry)));
346
+ }) }, icons.get(CommandIDs.openNewGeoJSONDialog)));
261
347
  //Add processing commands
262
- addProcessingCommands(app, commands, tracker, trans, formSchemaRegistry);
263
- commands.addCommand(CommandIDs.newHillshadeEntry, Object.assign({ label: trans.__('New Hillshade layer'), isEnabled: () => {
348
+ addProcessingCommands(app, commands, tracker, trans, formSchemaRegistry, Object.fromEntries(formSchemaRegistry.getSchemas()));
349
+ commands.addCommand(CommandIDs.openNewHillshadeDialog, Object.assign({ label: trans.__('Open New Hillshade Layer Creation Dialog'), caption: 'Open a dialog to create a new hillshade layer and source in the current JupyterGIS document.', describedBy: {
350
+ args: {
351
+ type: 'object',
352
+ properties: {},
353
+ },
354
+ }, isEnabled: () => {
264
355
  return tracker.currentWidget
265
356
  ? tracker.currentWidget.model.sharedModel.editable
266
357
  : false;
@@ -273,8 +364,13 @@ export function addCommands(app, tracker, translator, formSchemaRegistry, layerB
273
364
  layerData: { name: 'Custom Hillshade Layer' },
274
365
  sourceType: 'RasterDemSource',
275
366
  layerType: 'HillshadeLayer',
276
- }) }, icons.get(CommandIDs.newHillshadeEntry)));
277
- commands.addCommand(CommandIDs.newImageEntry, Object.assign({ label: trans.__('New Image layer'), isEnabled: () => {
367
+ }) }, icons.get(CommandIDs.openNewHillshadeDialog)));
368
+ commands.addCommand(CommandIDs.openNewImageDialog, Object.assign({ label: trans.__('Open New Image Layer Creation Dialog'), caption: 'Open a dialog to create a new image layer and source in the current JupyterGIS document.', describedBy: {
369
+ args: {
370
+ type: 'object',
371
+ properties: {},
372
+ },
373
+ }, isEnabled: () => {
278
374
  return tracker.currentWidget
279
375
  ? tracker.currentWidget.model.sharedModel.editable
280
376
  : false;
@@ -297,8 +393,13 @@ export function addCommands(app, tracker, translator, formSchemaRegistry, layerB
297
393
  layerData: { name: 'Custom Image Layer' },
298
394
  sourceType: 'ImageSource',
299
395
  layerType: 'ImageLayer',
300
- }) }, icons.get(CommandIDs.newImageEntry)));
301
- commands.addCommand(CommandIDs.newVideoEntry, Object.assign({ label: trans.__('New Video layer'), isEnabled: () => {
396
+ }) }, icons.get(CommandIDs.openNewImageDialog)));
397
+ commands.addCommand(CommandIDs.openNewVideoDialog, Object.assign({ label: trans.__('Open New Video Layer Creation Dialog'), caption: 'Open a dialog to create a new video layer and source in the current JupyterGIS document.', describedBy: {
398
+ args: {
399
+ type: 'object',
400
+ properties: {},
401
+ },
402
+ }, isEnabled: () => {
302
403
  return tracker.currentWidget
303
404
  ? tracker.currentWidget.model.sharedModel.editable
304
405
  : false;
@@ -324,8 +425,13 @@ export function addCommands(app, tracker, translator, formSchemaRegistry, layerB
324
425
  layerData: { name: 'Custom Video Layer' },
325
426
  sourceType: 'VideoSource',
326
427
  layerType: 'RasterLayer',
327
- }) }, icons.get(CommandIDs.newVideoEntry)));
328
- commands.addCommand(CommandIDs.newGeoTiffEntry, Object.assign({ label: trans.__('New GeoTiff layer'), isEnabled: () => {
428
+ }) }, icons.get(CommandIDs.openNewVideoDialog)));
429
+ commands.addCommand(CommandIDs.openNewGeoTiffDialog, Object.assign({ label: trans.__('Open New GeoTiff Layer Creation Dialog'), caption: 'Open a dialog to create a new GeoTiff layer and source in the current JupyterGIS document.', describedBy: {
430
+ args: {
431
+ type: 'object',
432
+ properties: {},
433
+ },
434
+ }, isEnabled: () => {
329
435
  return tracker.currentWidget
330
436
  ? tracker.currentWidget.model.sharedModel.editable
331
437
  : false;
@@ -342,8 +448,13 @@ export function addCommands(app, tracker, translator, formSchemaRegistry, layerB
342
448
  layerData: { name: 'Custom GeoTiff Layer' },
343
449
  sourceType: 'GeoTiffSource',
344
450
  layerType: 'WebGlLayer',
345
- }) }, icons.get(CommandIDs.newGeoTiffEntry)));
346
- commands.addCommand(CommandIDs.newShapefileEntry, Object.assign({ label: trans.__('New Shapefile Layer'), isEnabled: () => {
451
+ }) }, icons.get(CommandIDs.openNewGeoTiffDialog)));
452
+ commands.addCommand(CommandIDs.openNewShapefileDialog, Object.assign({ label: trans.__('Open New Shapefile Layer Creation Dialog'), caption: 'Open a dialog to create a new shapefile layer and source in the current JupyterGIS document.', describedBy: {
453
+ args: {
454
+ type: 'object',
455
+ properties: {},
456
+ },
457
+ }, isEnabled: () => {
347
458
  return tracker.currentWidget
348
459
  ? tracker.currentWidget.model.sharedModel.editable
349
460
  : false;
@@ -357,19 +468,16 @@ export function addCommands(app, tracker, translator, formSchemaRegistry, layerB
357
468
  layerData: { name: 'Custom Shapefile Layer' },
358
469
  sourceType: 'ShapefileSource',
359
470
  layerType: 'VectorLayer',
360
- }) }, icons.get(CommandIDs.newShapefileEntry)));
471
+ }) }, icons.get(CommandIDs.openNewShapefileDialog)));
361
472
  /**
362
473
  * LAYERS and LAYER GROUP actions.
363
474
  */
364
- commands.addCommand(CommandIDs.renameSelected, {
365
- label: trans.__('Rename'),
366
- isEnabled: () => {
475
+ commands.addCommand(CommandIDs.renameSelected, Object.assign({ label: trans.__('Rename'), isEnabled: () => {
367
476
  var _a, _b, _c;
368
477
  const model = (_a = tracker.currentWidget) === null || _a === void 0 ? void 0 : _a.model;
369
478
  const selected = (_c = (_b = model === null || model === void 0 ? void 0 : model.localState) === null || _b === void 0 ? void 0 : _b.selected) === null || _c === void 0 ? void 0 : _c.value;
370
479
  return !!selected && Object.keys(selected).length === 1;
371
- },
372
- execute: async () => {
480
+ }, execute: async () => {
373
481
  var _a, _b, _c;
374
482
  const model = (_a = tracker.currentWidget) === null || _a === void 0 ? void 0 : _a.model;
375
483
  const selected = (_c = (_b = model === null || model === void 0 ? void 0 : model.localState) === null || _b === void 0 ? void 0 : _b.selected) === null || _c === void 0 ? void 0 : _c.value;
@@ -377,17 +485,13 @@ export function addCommands(app, tracker, translator, formSchemaRegistry, layerB
377
485
  return;
378
486
  }
379
487
  await Private.renameSelectedItem(model);
380
- },
381
- });
382
- commands.addCommand(CommandIDs.removeSelected, {
383
- label: trans.__('Remove'),
384
- isEnabled: () => {
488
+ } }, icons.get(CommandIDs.renameSelected)));
489
+ commands.addCommand(CommandIDs.removeSelected, Object.assign({ label: trans.__('Remove'), isEnabled: () => {
385
490
  var _a, _b, _c;
386
491
  const model = (_a = tracker.currentWidget) === null || _a === void 0 ? void 0 : _a.model;
387
492
  const selected = (_c = (_b = model === null || model === void 0 ? void 0 : model.localState) === null || _b === void 0 ? void 0 : _b.selected) === null || _c === void 0 ? void 0 : _c.value;
388
493
  return !!selected && Object.keys(selected).length > 0;
389
- },
390
- execute: async () => {
494
+ }, execute: async () => {
391
495
  var _a, _b, _c;
392
496
  const model = (_a = tracker.currentWidget) === null || _a === void 0 ? void 0 : _a.model;
393
497
  const selected = (_c = (_b = model === null || model === void 0 ? void 0 : model.localState) === null || _b === void 0 ? void 0 : _b.selected) === null || _c === void 0 ? void 0 : _c.value;
@@ -395,27 +499,117 @@ export function addCommands(app, tracker, translator, formSchemaRegistry, layerB
395
499
  return;
396
500
  }
397
501
  await Private.removeSelectedItems(model);
502
+ } }, icons.get(CommandIDs.removeSelected)));
503
+ commands.addCommand(CommandIDs.duplicateSelected, {
504
+ label: trans.__('Duplicate'),
505
+ isEnabled: () => {
506
+ var _a, _b, _c;
507
+ const model = (_a = tracker.currentWidget) === null || _a === void 0 ? void 0 : _a.model;
508
+ const selected = (_c = (_b = model === null || model === void 0 ? void 0 : model.localState) === null || _b === void 0 ? void 0 : _b.selected) === null || _c === void 0 ? void 0 : _c.value;
509
+ if (!selected) {
510
+ return false;
511
+ }
512
+ return Object.values(selected).some(item => item.type === 'layer');
398
513
  },
399
- });
400
- commands.addCommand(CommandIDs.moveLayersToGroup, {
401
- label: args => args['label'] ? args['label'] : trans.__('Move to Root'),
402
- execute: args => {
514
+ execute: () => {
403
515
  var _a, _b, _c;
404
516
  const model = (_a = tracker.currentWidget) === null || _a === void 0 ? void 0 : _a.model;
405
- const groupName = args['label'];
406
- const selectedLayers = (_c = (_b = model === null || model === void 0 ? void 0 : model.localState) === null || _b === void 0 ? void 0 : _b.selected) === null || _c === void 0 ? void 0 : _c.value;
517
+ const selected = (_c = (_b = model === null || model === void 0 ? void 0 : model.localState) === null || _b === void 0 ? void 0 : _b.selected) === null || _c === void 0 ? void 0 : _c.value;
518
+ if (!model || !selected) {
519
+ return;
520
+ }
521
+ for (const [layerId, selectedItem] of Object.entries(selected)) {
522
+ if (selectedItem.type !== 'layer') {
523
+ continue;
524
+ }
525
+ const layer = model.getLayer(layerId);
526
+ if (!layer) {
527
+ continue;
528
+ }
529
+ const clonedLayer = Object.assign(Object.assign({}, layer), { name: Private.generateCopyName(layer.name, model) });
530
+ model.addLayer(UUID.uuid4(), clonedLayer);
531
+ }
532
+ },
533
+ });
534
+ commands.addCommand(CommandIDs.moveSelectedToGroup, {
535
+ label: args => args['label']
536
+ ? args['label']
537
+ : trans.__('Move Selection to Root Group'),
538
+ caption: 'Group layers together in a new group with name "groupName" for the JupyterGIS document "filepath"',
539
+ describedBy: {
540
+ args: {
541
+ type: 'object',
542
+ properties: {
543
+ label: { type: 'string' },
544
+ filePath: { type: 'string' },
545
+ layerIds: {
546
+ type: 'array',
547
+ items: { type: 'string' },
548
+ },
549
+ groupName: { type: 'string' },
550
+ },
551
+ },
552
+ },
553
+ execute: (args) => {
554
+ var _a, _b, _c, _d;
555
+ const { filePath, layerIds, groupName } = args !== null && args !== void 0 ? args : {};
556
+ // Resolve model based on filePath or current widget
557
+ const model = filePath
558
+ ? (_a = tracker.find(w => w.model.filePath === filePath)) === null || _a === void 0 ? void 0 : _a.model
559
+ : (_b = tracker.currentWidget) === null || _b === void 0 ? void 0 : _b.model;
560
+ if (!model || !model.sharedModel.editable) {
561
+ return;
562
+ }
563
+ if (filePath && layerIds && groupName !== undefined) {
564
+ model.moveItemsToGroup(layerIds, groupName);
565
+ return;
566
+ }
567
+ const selectedLayers = (_d = (_c = model.localState) === null || _c === void 0 ? void 0 : _c.selected) === null || _d === void 0 ? void 0 : _d.value;
407
568
  if (!selectedLayers) {
408
569
  return;
409
570
  }
410
- model.moveItemsToGroup(Object.keys(selectedLayers), groupName);
571
+ const targetGroup = args === null || args === void 0 ? void 0 : args.label;
572
+ model.moveItemsToGroup(Object.keys(selectedLayers), targetGroup);
411
573
  },
412
574
  });
413
- commands.addCommand(CommandIDs.moveLayerToNewGroup, {
414
- label: trans.__('Move Selected Layers to New Group'),
415
- execute: async () => {
416
- var _a, _b, _c;
417
- const model = (_a = tracker.currentWidget) === null || _a === void 0 ? void 0 : _a.model;
418
- const selectedLayers = (_c = (_b = model === null || model === void 0 ? void 0 : model.localState) === null || _b === void 0 ? void 0 : _b.selected) === null || _c === void 0 ? void 0 : _c.value;
575
+ commands.addCommand(CommandIDs.moveSelectedToNewGroup, {
576
+ label: trans.__('Move Selection to New Group'),
577
+ caption: 'Move selected layers to a new group in the current JupyterGIS document.',
578
+ describedBy: {
579
+ args: {
580
+ type: 'object',
581
+ properties: {
582
+ filePath: { type: 'string' },
583
+ groupName: { type: 'string' },
584
+ layerIds: {
585
+ type: 'array',
586
+ items: { type: 'string' },
587
+ },
588
+ },
589
+ },
590
+ },
591
+ execute: async (args) => {
592
+ var _a, _b, _c, _d;
593
+ const { filePath, groupName, layerIds } = args !== null && args !== void 0 ? args : {};
594
+ const model = filePath
595
+ ? (_a = tracker.find(w => w.model.filePath === filePath)) === null || _a === void 0 ? void 0 : _a.model
596
+ : (_b = tracker.currentWidget) === null || _b === void 0 ? void 0 : _b.model;
597
+ if (!model || !model.sharedModel.editable) {
598
+ return;
599
+ }
600
+ if (filePath && groupName && layerIds) {
601
+ const layerMap = {};
602
+ layerIds.forEach(id => {
603
+ layerMap[id] = { type: 'layer', selectedNodeId: id };
604
+ });
605
+ const newGroup = {
606
+ name: groupName,
607
+ layers: layerIds,
608
+ };
609
+ model.addNewLayerGroup(layerMap, newGroup);
610
+ return;
611
+ }
612
+ const selectedLayers = (_d = (_c = model.localState) === null || _c === void 0 ? void 0 : _c.selected) === null || _d === void 0 ? void 0 : _d.value;
419
613
  if (!selectedLayers) {
420
614
  return;
421
615
  }
@@ -464,28 +658,16 @@ export function addCommands(app, tracker, translator, formSchemaRegistry, layerB
464
658
  model.addNewLayerGroup(selectedLayers, newLayerGroup);
465
659
  },
466
660
  });
467
- /**
468
- * Source actions
469
- */
470
- commands.addCommand(CommandIDs.renameSource, {
471
- label: trans.__('Rename Source'),
472
- execute: async () => {
473
- var _a;
474
- const model = (_a = tracker.currentWidget) === null || _a === void 0 ? void 0 : _a.model;
475
- await Private.renameSelectedItem(model);
476
- },
477
- });
478
- commands.addCommand(CommandIDs.removeSource, {
479
- label: trans.__('Remove Source'),
480
- execute: () => {
481
- var _a;
482
- const model = (_a = tracker.currentWidget) === null || _a === void 0 ? void 0 : _a.model;
483
- Private.removeSelectedSources(model);
484
- },
485
- });
486
661
  // Console commands
487
662
  commands.addCommand(CommandIDs.toggleConsole, {
488
663
  label: trans.__('Toggle console'),
664
+ caption: 'Toggle the console in the current JupyterGIS document.',
665
+ describedBy: {
666
+ args: {
667
+ type: 'object',
668
+ properties: {},
669
+ },
670
+ },
489
671
  isVisible: () => tracker.currentWidget instanceof JupyterGISDocumentWidget,
490
672
  isEnabled: () => {
491
673
  return tracker.currentWidget
@@ -508,6 +690,13 @@ export function addCommands(app, tracker, translator, formSchemaRegistry, layerB
508
690
  });
509
691
  commands.addCommand(CommandIDs.executeConsole, {
510
692
  label: trans.__('Execute console'),
693
+ caption: 'Execute the console in the current JupyterGIS document.',
694
+ describedBy: {
695
+ args: {
696
+ type: 'object',
697
+ properties: {},
698
+ },
699
+ },
511
700
  isVisible: () => tracker.currentWidget instanceof JupyterGISDocumentWidget,
512
701
  isEnabled: () => {
513
702
  return tracker.currentWidget
@@ -518,6 +707,13 @@ export function addCommands(app, tracker, translator, formSchemaRegistry, layerB
518
707
  });
519
708
  commands.addCommand(CommandIDs.removeConsole, {
520
709
  label: trans.__('Remove console'),
710
+ caption: 'Remove the console from the current JupyterGIS document.',
711
+ describedBy: {
712
+ args: {
713
+ type: 'object',
714
+ properties: {},
715
+ },
716
+ },
521
717
  isVisible: () => tracker.currentWidget instanceof JupyterGISDocumentWidget,
522
718
  isEnabled: () => {
523
719
  return tracker.currentWidget
@@ -528,6 +724,13 @@ export function addCommands(app, tracker, translator, formSchemaRegistry, layerB
528
724
  });
529
725
  commands.addCommand(CommandIDs.invokeCompleter, {
530
726
  label: trans.__('Display the completion helper.'),
727
+ caption: 'Display the completion helper in the current JupyterGIS document.',
728
+ describedBy: {
729
+ args: {
730
+ type: 'object',
731
+ properties: {},
732
+ },
733
+ },
531
734
  isVisible: () => tracker.currentWidget instanceof JupyterGISDocumentWidget,
532
735
  execute: () => {
533
736
  var _a;
@@ -545,6 +748,13 @@ export function addCommands(app, tracker, translator, formSchemaRegistry, layerB
545
748
  });
546
749
  commands.addCommand(CommandIDs.selectCompleter, {
547
750
  label: trans.__('Select the completion suggestion.'),
751
+ caption: 'Select the completion suggestion in the current JupyterGIS document.',
752
+ describedBy: {
753
+ args: {
754
+ type: 'object',
755
+ properties: {},
756
+ },
757
+ },
548
758
  isVisible: () => tracker.currentWidget instanceof JupyterGISDocumentWidget,
549
759
  execute: () => {
550
760
  var _a;
@@ -562,37 +772,98 @@ export function addCommands(app, tracker, translator, formSchemaRegistry, layerB
562
772
  });
563
773
  commands.addCommand(CommandIDs.zoomToLayer, {
564
774
  label: trans.__('Zoom to Layer'),
565
- execute: () => {
566
- var _a;
567
- const currentWidget = tracker.currentWidget;
568
- if (!currentWidget || !completionProviderManager) {
775
+ caption: 'Zoom to the selected layer in the current JupyterGIS document.',
776
+ describedBy: {
777
+ args: {
778
+ type: 'object',
779
+ properties: {
780
+ filePath: { type: 'string' },
781
+ layerId: { type: 'string' },
782
+ },
783
+ },
784
+ },
785
+ execute: (args) => {
786
+ var _a, _b;
787
+ const { filePath, layerId } = args !== null && args !== void 0 ? args : {};
788
+ // Determine model from provided file path or fallback to current widget
789
+ const current = filePath
790
+ ? tracker.find(w => w.model.filePath === filePath)
791
+ : tracker.currentWidget;
792
+ if (!current || !current.model.sharedModel.editable) {
793
+ return;
794
+ }
795
+ const model = current.model;
796
+ if (filePath && layerId) {
797
+ model.centerOnPosition(layerId);
569
798
  return;
570
799
  }
571
- const model = tracker.currentWidget.model;
572
- const selectedItems = (_a = model.localState) === null || _a === void 0 ? void 0 : _a.selected.value;
800
+ const selectedItems = (_b = (_a = model.localState) === null || _a === void 0 ? void 0 : _a.selected) === null || _b === void 0 ? void 0 : _b.value;
573
801
  if (!selectedItems) {
574
802
  return;
575
803
  }
576
- const layerId = Object.keys(selectedItems)[0];
577
- model.centerOnPosition(layerId);
804
+ const selLayerId = Object.keys(selectedItems)[0];
805
+ model.centerOnPosition(selLayerId);
578
806
  },
579
807
  });
580
808
  commands.addCommand(CommandIDs.downloadGeoJSON, {
581
809
  label: trans.__('Download as GeoJSON'),
810
+ caption: 'Download the selected layer as a GeoJSON file in the current JupyterGIS document.',
811
+ describedBy: {
812
+ args: {
813
+ type: 'object',
814
+ properties: {
815
+ filePath: { type: 'string' },
816
+ layerId: { type: 'string' },
817
+ exportFileName: { type: 'string' },
818
+ },
819
+ },
820
+ },
582
821
  isEnabled: () => {
583
- const selectedLayer = getSingleSelectedLayer(tracker);
584
- return selectedLayer
585
- ? ['VectorLayer', 'ShapefileLayer'].includes(selectedLayer.type)
586
- : false;
822
+ const layer = getSingleSelectedLayer(tracker);
823
+ return !!layer && ['VectorLayer', 'ShapefileLayer'].includes(layer.type);
587
824
  },
588
- execute: async () => {
589
- var _a, _b;
825
+ execute: async (args) => {
826
+ var _a;
827
+ const exportLayer = async (model, layer, exportFileName) => {
828
+ var _a, _b;
829
+ if (!['VectorLayer', 'ShapefileLayer'].includes(layer.type)) {
830
+ console.warn('Layer type not supported for GeoJSON export');
831
+ return;
832
+ }
833
+ const sources = (_a = model.sharedModel.sources) !== null && _a !== void 0 ? _a : {};
834
+ const sourceId = (_b = layer.parameters) === null || _b === void 0 ? void 0 : _b.source;
835
+ const source = sources[sourceId];
836
+ if (!source) {
837
+ console.warn('Source not found for selected layer');
838
+ return;
839
+ }
840
+ const geojsonString = await getGeoJSONDataFromLayerSource(source, model);
841
+ if (!geojsonString) {
842
+ console.warn('Failed to generate GeoJSON data');
843
+ return;
844
+ }
845
+ downloadFile(geojsonString, `${exportFileName}.geojson`, 'application/geo+json');
846
+ };
847
+ const { filePath, layerId, exportFileName } = args !== null && args !== void 0 ? args : {};
848
+ if (filePath && layerId && exportFileName) {
849
+ const widget = tracker.find(w => w.model.filePath === filePath);
850
+ if (!widget || !widget.model.sharedModel.editable) {
851
+ console.warn('Invalid or non-editable document');
852
+ return;
853
+ }
854
+ const model = widget.model;
855
+ const layer = model.getLayer(layerId);
856
+ if (!layer) {
857
+ console.warn('Layer not found');
858
+ return;
859
+ }
860
+ return exportLayer(model, layer, exportFileName);
861
+ }
590
862
  const selectedLayer = getSingleSelectedLayer(tracker);
591
- if (!selectedLayer) {
863
+ const model = (_a = tracker.currentWidget) === null || _a === void 0 ? void 0 : _a.model;
864
+ if (!selectedLayer || !model) {
592
865
  return;
593
866
  }
594
- const model = (_a = tracker.currentWidget) === null || _a === void 0 ? void 0 : _a.model;
595
- const sources = (_b = model.sharedModel.sources) !== null && _b !== void 0 ? _b : {};
596
867
  const exportSchema = Object.assign({}, formSchemaRegistry
597
868
  .getSchemas()
598
869
  .get('ExportGeoJSONSchema'));
@@ -614,21 +885,31 @@ export function addCommands(app, tracker, translator, formSchemaRegistry, layerB
614
885
  if (!formValues || !selectedLayer.parameters) {
615
886
  return;
616
887
  }
617
- const exportFileName = formValues.exportFileName;
618
- const sourceId = selectedLayer.parameters.source;
619
- const source = sources[sourceId];
620
- const geojsonString = await getGeoJSONDataFromLayerSource(source, model);
621
- if (!geojsonString) {
622
- return;
623
- }
624
- downloadFile(geojsonString, `${exportFileName}.geojson`, 'application/geo+json');
888
+ return exportLayer(model, selectedLayer, formValues.exportFileName);
625
889
  },
626
890
  });
627
891
  commands.addCommand(CommandIDs.getGeolocation, {
628
892
  label: trans.__('Center on Geolocation'),
629
- execute: async () => {
630
- var _a;
631
- const viewModel = (_a = tracker.currentWidget) === null || _a === void 0 ? void 0 : _a.model;
893
+ caption: "Center the map on the user's current geolocation.",
894
+ describedBy: {
895
+ args: {
896
+ type: 'object',
897
+ properties: {
898
+ filePath: { type: 'string' },
899
+ },
900
+ },
901
+ },
902
+ execute: async (args) => {
903
+ const { filePath } = args !== null && args !== void 0 ? args : {};
904
+ // Resolve widget once
905
+ const current = filePath
906
+ ? tracker.find(w => w.model.filePath === filePath)
907
+ : tracker.currentWidget;
908
+ if (!current) {
909
+ console.warn('No document found');
910
+ return;
911
+ }
912
+ const viewModel = current.model;
632
913
  const options = {
633
914
  enableHighAccuracy: true,
634
915
  timeout: 5000,
@@ -639,16 +920,14 @@ export function addCommands(app, tracker, translator, formSchemaRegistry, layerB
639
920
  pos.coords.longitude,
640
921
  pos.coords.latitude,
641
922
  ]);
642
- const Jgislocation = {
923
+ const jgisLocation = {
643
924
  x: location[0],
644
925
  y: location[1],
645
926
  };
646
- if (viewModel) {
647
- viewModel.geolocationChanged.emit(Jgislocation);
648
- }
927
+ viewModel.geolocationChanged.emit(jgisLocation);
649
928
  };
650
929
  const error = (err) => {
651
- console.warn(`ERROR(${err.code}): ${err.message}`);
930
+ console.warn(`Geolocation error (${err.code}): ${err.message}`);
652
931
  };
653
932
  navigator.geolocation.getCurrentPosition(success, error, options);
654
933
  },
@@ -657,6 +936,13 @@ export function addCommands(app, tracker, translator, formSchemaRegistry, layerB
657
936
  // Panel visibility commands
658
937
  commands.addCommand(CommandIDs.toggleLeftPanel, {
659
938
  label: trans.__('Toggle Left Panel'),
939
+ caption: 'Toggle the left panel in the current JupyterGIS document.',
940
+ describedBy: {
941
+ args: {
942
+ type: 'object',
943
+ properties: {},
944
+ },
945
+ },
660
946
  isEnabled: () => Boolean(tracker.currentWidget),
661
947
  isToggled: () => {
662
948
  const current = tracker.currentWidget;
@@ -681,6 +967,13 @@ export function addCommands(app, tracker, translator, formSchemaRegistry, layerB
681
967
  });
682
968
  commands.addCommand(CommandIDs.toggleRightPanel, {
683
969
  label: trans.__('Toggle Right Panel'),
970
+ caption: 'Toggle the right panel in the current JupyterGIS document.',
971
+ describedBy: {
972
+ args: {
973
+ type: 'object',
974
+ properties: {},
975
+ },
976
+ },
684
977
  isEnabled: () => Boolean(tracker.currentWidget),
685
978
  isToggled: () => {
686
979
  const current = tracker.currentWidget;
@@ -706,6 +999,13 @@ export function addCommands(app, tracker, translator, formSchemaRegistry, layerB
706
999
  // Left panel tabs
707
1000
  commands.addCommand(CommandIDs.showLayersTab, {
708
1001
  label: trans.__('Show Layers Tab'),
1002
+ caption: 'Show the layers tab in the current JupyterGIS document.',
1003
+ describedBy: {
1004
+ args: {
1005
+ type: 'object',
1006
+ properties: {},
1007
+ },
1008
+ },
709
1009
  isEnabled: () => Boolean(tracker.currentWidget),
710
1010
  isToggled: () => tracker.currentWidget
711
1011
  ? !tracker.currentWidget.model.jgisSettings.layersDisabled
@@ -724,6 +1024,13 @@ export function addCommands(app, tracker, translator, formSchemaRegistry, layerB
724
1024
  });
725
1025
  commands.addCommand(CommandIDs.showStacBrowserTab, {
726
1026
  label: trans.__('Show STAC Browser Tab'),
1027
+ caption: 'Show the STAC browser tab in the current JupyterGIS document.',
1028
+ describedBy: {
1029
+ args: {
1030
+ type: 'object',
1031
+ properties: {},
1032
+ },
1033
+ },
727
1034
  isEnabled: () => Boolean(tracker.currentWidget),
728
1035
  isToggled: () => tracker.currentWidget
729
1036
  ? !tracker.currentWidget.model.jgisSettings.stacBrowserDisabled
@@ -742,6 +1049,13 @@ export function addCommands(app, tracker, translator, formSchemaRegistry, layerB
742
1049
  });
743
1050
  commands.addCommand(CommandIDs.showFiltersTab, {
744
1051
  label: trans.__('Show Filters Tab'),
1052
+ caption: 'Show the filters tab in the current JupyterGIS document.',
1053
+ describedBy: {
1054
+ args: {
1055
+ type: 'object',
1056
+ properties: {},
1057
+ },
1058
+ },
745
1059
  isEnabled: () => Boolean(tracker.currentWidget),
746
1060
  isToggled: () => tracker.currentWidget
747
1061
  ? !tracker.currentWidget.model.jgisSettings.filtersDisabled
@@ -761,6 +1075,13 @@ export function addCommands(app, tracker, translator, formSchemaRegistry, layerB
761
1075
  // Right panel tabs
762
1076
  commands.addCommand(CommandIDs.showObjectPropertiesTab, {
763
1077
  label: trans.__('Show Object Properties Tab'),
1078
+ caption: 'Show the object properties tab in the current JupyterGIS document.',
1079
+ describedBy: {
1080
+ args: {
1081
+ type: 'object',
1082
+ properties: {},
1083
+ },
1084
+ },
764
1085
  isEnabled: () => Boolean(tracker.currentWidget),
765
1086
  isToggled: () => tracker.currentWidget
766
1087
  ? !tracker.currentWidget.model.jgisSettings.objectPropertiesDisabled
@@ -779,6 +1100,13 @@ export function addCommands(app, tracker, translator, formSchemaRegistry, layerB
779
1100
  });
780
1101
  commands.addCommand(CommandIDs.showAnnotationsTab, {
781
1102
  label: trans.__('Show Annotations Tab'),
1103
+ caption: 'Show the annotations tab in the current JupyterGIS document.',
1104
+ describedBy: {
1105
+ args: {
1106
+ type: 'object',
1107
+ properties: {},
1108
+ },
1109
+ },
782
1110
  isEnabled: () => Boolean(tracker.currentWidget),
783
1111
  isToggled: () => tracker.currentWidget
784
1112
  ? !tracker.currentWidget.model.jgisSettings.annotationsDisabled
@@ -797,6 +1125,13 @@ export function addCommands(app, tracker, translator, formSchemaRegistry, layerB
797
1125
  });
798
1126
  commands.addCommand(CommandIDs.showIdentifyPanelTab, {
799
1127
  label: trans.__('Show Identify Panel Tab'),
1128
+ caption: 'Show the identify panel tab in the current JupyterGIS document.',
1129
+ describedBy: {
1130
+ args: {
1131
+ type: 'object',
1132
+ properties: {},
1133
+ },
1134
+ },
800
1135
  isEnabled: () => Boolean(tracker.currentWidget),
801
1136
  isToggled: () => tracker.currentWidget
802
1137
  ? !tracker.currentWidget.model.jgisSettings.identifyDisabled
@@ -871,6 +1206,63 @@ export function addCommands(app, tracker, translator, formSchemaRegistry, layerB
871
1206
  current.model.setOptions(Object.assign(Object.assign({}, currentOptions), { storyMapPresentationMode: !currentOptions.storyMapPresentationMode }));
872
1207
  commands.notifyCommandChanged(CommandIDs.toggleStoryPresentationMode);
873
1208
  } }, icons.get(CommandIDs.toggleStoryPresentationMode)));
1209
+ /* Needs to be enabled in Specta mode, so add without Specta-aware wrapper */
1210
+ originalAddCommand(CommandIDs.storyPrev, {
1211
+ label: trans.__('Previous Story Segment'),
1212
+ isEnabled: () => {
1213
+ var _a, _b;
1214
+ const model = (_a = tracker.currentWidget) === null || _a === void 0 ? void 0 : _a.model;
1215
+ const storySegments = (_b = model === null || model === void 0 ? void 0 : model.getSelectedStory().story) === null || _b === void 0 ? void 0 : _b.storySegments;
1216
+ if (!model ||
1217
+ model.jgisSettings.storyMapsDisabled ||
1218
+ !storySegments ||
1219
+ storySegments.length < 1) {
1220
+ return false;
1221
+ }
1222
+ if (!model.isSpectaMode()) {
1223
+ return false;
1224
+ }
1225
+ return model.getCurrentSegmentIndex() > 0;
1226
+ },
1227
+ execute: () => {
1228
+ var _a, _b;
1229
+ const model = (_a = tracker.currentWidget) === null || _a === void 0 ? void 0 : _a.model;
1230
+ if (!model) {
1231
+ return;
1232
+ }
1233
+ const current = (_b = model.getCurrentSegmentIndex()) !== null && _b !== void 0 ? _b : 0;
1234
+ model.setCurrentSegmentIndex(current - 1);
1235
+ },
1236
+ });
1237
+ originalAddCommand(CommandIDs.storyNext, {
1238
+ label: trans.__('Next Story Segment'),
1239
+ isEnabled: () => {
1240
+ var _a, _b, _c;
1241
+ const model = (_a = tracker.currentWidget) === null || _a === void 0 ? void 0 : _a.model;
1242
+ const storySegments = (_b = model === null || model === void 0 ? void 0 : model.getSelectedStory().story) === null || _b === void 0 ? void 0 : _b.storySegments;
1243
+ if (!model ||
1244
+ model.jgisSettings.storyMapsDisabled ||
1245
+ !storySegments ||
1246
+ storySegments.length < 1) {
1247
+ return false;
1248
+ }
1249
+ const isSpecta = model.isSpectaMode();
1250
+ if (!isSpecta) {
1251
+ return false;
1252
+ }
1253
+ const current = (_c = model.getCurrentSegmentIndex()) !== null && _c !== void 0 ? _c : 0;
1254
+ return current < storySegments.length - 1;
1255
+ },
1256
+ execute: () => {
1257
+ var _a, _b;
1258
+ const model = (_a = tracker.currentWidget) === null || _a === void 0 ? void 0 : _a.model;
1259
+ if (!model) {
1260
+ return;
1261
+ }
1262
+ const current = (_b = model.getCurrentSegmentIndex()) !== null && _b !== void 0 ? _b : 0;
1263
+ model.setCurrentSegmentIndex(current + 1);
1264
+ },
1265
+ });
874
1266
  loadKeybindings(commands, keybindings);
875
1267
  }
876
1268
  var Private;
@@ -964,21 +1356,6 @@ var Private;
964
1356
  model.setEditingItem(item.type, itemId);
965
1357
  }
966
1358
  Private.renameSelectedItem = renameSelectedItem;
967
- function removeSelectedSources(model) {
968
- var _a, _b;
969
- const selected = (_b = (_a = model === null || model === void 0 ? void 0 : model.localState) === null || _a === void 0 ? void 0 : _a.selected) === null || _b === void 0 ? void 0 : _b.value;
970
- if (!selected || !model) {
971
- return;
972
- }
973
- for (const id of Object.keys(selected)) {
974
- if (model.getLayersBySource(id).length > 0) {
975
- showErrorMessage('Remove source error', 'The source is used by a layer.');
976
- continue;
977
- }
978
- model.sharedModel.removeSource(id);
979
- }
980
- }
981
- Private.removeSelectedSources = removeSelectedSources;
982
1359
  function executeConsole(tracker) {
983
1360
  const current = tracker.currentWidget;
984
1361
  if (!current || !(current instanceof JupyterGISDocumentWidget)) {
@@ -1003,4 +1380,25 @@ var Private;
1003
1380
  await current.content.toggleConsole(current.model.filePath);
1004
1381
  }
1005
1382
  Private.toggleConsole = toggleConsole;
1383
+ function generateCopyName(baseName, model) {
1384
+ const layers = model.getLayers();
1385
+ const existingNames = Object.values(layers).map(l => l.name);
1386
+ const copyRegex = /(.*?)( Copy(_\d+)?)?$/;
1387
+ const match = baseName.match(copyRegex);
1388
+ const cleanBase = match ? match[1].trim() : baseName;
1389
+ const firstCopyName = `${cleanBase} Copy`;
1390
+ if (!existingNames.includes(firstCopyName)) {
1391
+ return firstCopyName;
1392
+ }
1393
+ const pattern = new RegExp(`^${cleanBase} Copy_(\\d+)$`);
1394
+ const numbers = existingNames
1395
+ .map(name => {
1396
+ const m = name.match(pattern);
1397
+ return m ? parseInt(m[1], 10) : null;
1398
+ })
1399
+ .filter((n) => n !== null);
1400
+ const nextNumber = numbers.length > 0 ? Math.max(...numbers) + 1 : 1;
1401
+ return `${cleanBase} Copy_${nextNumber}`;
1402
+ }
1403
+ Private.generateCopyName = generateCopyName;
1006
1404
  })(Private || (Private = {}));