@jupytergis/base 0.11.1 → 0.12.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 (101) hide show
  1. package/lib/commands/BaseCommandIDs.d.ts +1 -0
  2. package/lib/commands/BaseCommandIDs.js +1 -0
  3. package/lib/commands/index.js +52 -0
  4. package/lib/constants.js +3 -0
  5. package/lib/dialogs/symbology/hooks/useGetBandInfo.d.ts +0 -6
  6. package/lib/dialogs/symbology/hooks/useGetBandInfo.js +2 -2
  7. package/lib/dialogs/symbology/tiff_layer/types/MultibandColor.js +4 -4
  8. package/lib/formbuilder/objectform/StoryEditorForm.d.ts +3 -2
  9. package/lib/formbuilder/objectform/StoryEditorForm.js +7 -1
  10. package/lib/mainview/mainView.d.ts +18 -0
  11. package/lib/mainview/mainView.js +243 -18
  12. package/lib/panelview/{components/filter-panel → filter-panel}/Filter.js +1 -1
  13. package/lib/panelview/leftpanel.js +4 -4
  14. package/lib/panelview/rightpanel.d.ts +2 -0
  15. package/lib/panelview/rightpanel.js +21 -14
  16. package/lib/panelview/{components/story-maps → story-maps}/PreviewModeSwitch.js +3 -2
  17. package/lib/panelview/story-maps/StoryEditorPanel.d.ts +9 -0
  18. package/lib/panelview/story-maps/StoryEditorPanel.js +34 -0
  19. package/lib/panelview/{components/story-maps → story-maps}/StoryNavBar.d.ts +2 -1
  20. package/lib/panelview/{components/story-maps → story-maps}/StoryNavBar.js +3 -3
  21. package/lib/panelview/story-maps/StoryViewerPanel.d.ts +13 -0
  22. package/lib/panelview/{components/story-maps → story-maps}/StoryViewerPanel.js +37 -24
  23. package/lib/panelview/story-maps/components/StoryContentSection.d.ts +6 -0
  24. package/lib/panelview/story-maps/components/StoryContentSection.js +10 -0
  25. package/lib/panelview/story-maps/components/StoryImageSection.d.ts +15 -0
  26. package/lib/panelview/story-maps/components/StoryImageSection.js +13 -0
  27. package/lib/panelview/story-maps/components/StorySubtitleSection.d.ts +11 -0
  28. package/lib/panelview/story-maps/components/StorySubtitleSection.js +9 -0
  29. package/lib/panelview/story-maps/components/StoryTitleSection.d.ts +12 -0
  30. package/lib/panelview/story-maps/components/StoryTitleSection.js +8 -0
  31. package/lib/shared/components/Combobox.d.ts +21 -0
  32. package/lib/shared/components/Combobox.js +32 -0
  33. package/lib/shared/components/Command.js +10 -10
  34. package/lib/shared/components/Input.d.ts +3 -0
  35. package/lib/shared/components/Input.js +18 -0
  36. package/lib/shared/components/Pagination.js +3 -2
  37. package/lib/shared/components/Select.d.ts +19 -0
  38. package/lib/shared/components/Select.js +28 -0
  39. package/lib/shared/components/SingleDatePicker.d.ts +11 -0
  40. package/lib/shared/components/SingleDatePicker.js +16 -0
  41. package/lib/stacBrowser/components/StacPanel.d.ts +9 -1
  42. package/lib/stacBrowser/components/StacPanel.js +53 -9
  43. package/lib/stacBrowser/components/filter-extension/QueryableComboBox.d.ts +9 -0
  44. package/lib/stacBrowser/components/filter-extension/QueryableComboBox.js +179 -0
  45. package/lib/stacBrowser/components/filter-extension/QueryableRow.d.ts +16 -0
  46. package/lib/stacBrowser/components/filter-extension/QueryableRow.js +16 -0
  47. package/lib/stacBrowser/components/filter-extension/StacFilterExtensionPanel.d.ts +7 -0
  48. package/lib/stacBrowser/components/filter-extension/StacFilterExtensionPanel.js +49 -0
  49. package/lib/stacBrowser/components/filter-extension/StacQueryableFilters.d.ts +11 -0
  50. package/lib/stacBrowser/components/filter-extension/StacQueryableFilters.js +19 -0
  51. package/lib/stacBrowser/components/{StacFilterSection.d.ts → geodes/StacFilterSection.d.ts} +1 -1
  52. package/lib/stacBrowser/components/{StacFilterSection.js → geodes/StacFilterSection.js} +3 -3
  53. package/lib/stacBrowser/components/geodes/StacGeodesFilterPanel.d.ts +7 -0
  54. package/lib/stacBrowser/components/geodes/StacGeodesFilterPanel.js +69 -0
  55. package/lib/stacBrowser/components/shared/StacPanelResults.d.ts +3 -0
  56. package/lib/stacBrowser/components/shared/StacPanelResults.js +68 -0
  57. package/lib/stacBrowser/components/shared/StacSpatialExtent.d.ts +8 -0
  58. package/lib/stacBrowser/components/shared/StacSpatialExtent.js +10 -0
  59. package/lib/stacBrowser/components/shared/StacTemporalExtent.d.ts +9 -0
  60. package/lib/stacBrowser/components/shared/StacTemporalExtent.js +9 -0
  61. package/lib/stacBrowser/context/StacResultsContext.d.ts +33 -0
  62. package/lib/stacBrowser/context/StacResultsContext.js +269 -0
  63. package/lib/stacBrowser/hooks/useGeodesSearch.d.ts +24 -0
  64. package/lib/stacBrowser/hooks/useGeodesSearch.js +178 -0
  65. package/lib/stacBrowser/hooks/useStacFilterExtension.d.ts +30 -0
  66. package/lib/stacBrowser/hooks/useStacFilterExtension.js +262 -0
  67. package/lib/stacBrowser/hooks/useStacSearch.d.ts +5 -16
  68. package/lib/stacBrowser/hooks/useStacSearch.js +30 -184
  69. package/lib/stacBrowser/types/types.d.ts +86 -3
  70. package/lib/toolbar/widget.d.ts +5 -0
  71. package/lib/toolbar/widget.js +23 -2
  72. package/lib/tools.d.ts +0 -7
  73. package/lib/tools.js +55 -14
  74. package/package.json +2 -2
  75. package/style/base.css +38 -3
  76. package/style/shared/button.css +5 -4
  77. package/style/shared/calendar.css +7 -1
  78. package/style/shared/combobox.css +75 -0
  79. package/style/shared/command.css +178 -0
  80. package/style/shared/input.css +59 -0
  81. package/style/shared/pagination.css +1 -1
  82. package/style/shared/popover.css +1 -0
  83. package/style/shared/tabs.css +1 -1
  84. package/style/shared/toggle.css +1 -1
  85. package/style/stacBrowser.css +169 -16
  86. package/style/statusBar.css +1 -0
  87. package/style/storyPanel.css +120 -3
  88. package/style/tabPanel.css +0 -86
  89. package/lib/panelview/components/story-maps/StoryEditorPanel.d.ts +0 -7
  90. package/lib/panelview/components/story-maps/StoryEditorPanel.js +0 -29
  91. package/lib/panelview/components/story-maps/StoryViewerPanel.d.ts +0 -7
  92. package/lib/stacBrowser/components/StacPanelFilters.d.ts +0 -14
  93. package/lib/stacBrowser/components/StacPanelFilters.js +0 -81
  94. package/lib/stacBrowser/components/StacPanelResults.d.ts +0 -13
  95. package/lib/stacBrowser/components/StacPanelResults.js +0 -48
  96. /package/lib/panelview/{components/filter-panel → filter-panel}/Filter.d.ts +0 -0
  97. /package/lib/panelview/{components/filter-panel → filter-panel}/FilterRow.d.ts +0 -0
  98. /package/lib/panelview/{components/filter-panel → filter-panel}/FilterRow.js +0 -0
  99. /package/lib/panelview/{components/identify-panel → identify-panel}/IdentifyPanel.d.ts +0 -0
  100. /package/lib/panelview/{components/identify-panel → identify-panel}/IdentifyPanel.js +0 -0
  101. /package/lib/panelview/{components/story-maps → story-maps}/PreviewModeSwitch.d.ts +0 -0
@@ -41,3 +41,4 @@ export declare const showObjectPropertiesTab = "jupytergis:showObjectPropertiesT
41
41
  export declare const showAnnotationsTab = "jupytergis:showAnnotationsTab";
42
42
  export declare const showIdentifyPanelTab = "jupytergis:showIdentifyPanelTab";
43
43
  export declare const addStorySegment = "jupytergis:addStorySegment";
44
+ export declare const toggleStoryPresentationMode = "jupytergis:toggleStoryPresentationMode";
@@ -57,3 +57,4 @@ export const showAnnotationsTab = 'jupytergis:showAnnotationsTab';
57
57
  export const showIdentifyPanelTab = 'jupytergis:showIdentifyPanelTab';
58
58
  // Story maps
59
59
  export const addStorySegment = 'jupytergis:addStorySegment';
60
+ export const toggleStoryPresentationMode = 'jupytergis:toggleStoryPresentationMode';
@@ -28,6 +28,31 @@ export function addCommands(app, tracker, translator, formSchemaRegistry, layerB
28
28
  var _a;
29
29
  const trans = translator.load('jupyterlab');
30
30
  const { commands } = app;
31
+ /**
32
+ * Wraps a command definition to automatically disable it in Specta mode
33
+ */
34
+ const createSpectaAwareCommand = (command) => {
35
+ const originalIsEnabled = command.isEnabled;
36
+ return Object.assign(Object.assign({}, command), { isEnabled: (args) => {
37
+ var _a;
38
+ // First check if we're in Specta mode
39
+ const currentModel = (_a = tracker.currentWidget) === null || _a === void 0 ? void 0 : _a.model;
40
+ if (currentModel === null || currentModel === void 0 ? void 0 : currentModel.isSpectaMode()) {
41
+ return false;
42
+ }
43
+ // Then check the original isEnabled if it exists
44
+ if (originalIsEnabled) {
45
+ return originalIsEnabled(args !== null && args !== void 0 ? args : {});
46
+ }
47
+ // Default to enabled if no original check
48
+ return true;
49
+ } });
50
+ };
51
+ // Override addCommand to automatically wrap all commands
52
+ const originalAddCommand = commands.addCommand.bind(commands);
53
+ commands.addCommand = (id, options) => {
54
+ return originalAddCommand(id, createSpectaAwareCommand(options));
55
+ };
31
56
  commands.addCommand(CommandIDs.symbology, Object.assign({ label: trans.__('Edit Symbology'), isEnabled: () => {
32
57
  var _a, _b;
33
58
  const model = (_a = tracker.currentWidget) === null || _a === void 0 ? void 0 : _a.model;
@@ -356,6 +381,7 @@ export function addCommands(app, tracker, translator, formSchemaRegistry, layerB
356
381
  Private.removeSelectedItems(model, 'layer', selection => {
357
382
  model === null || model === void 0 ? void 0 : model.removeLayer(selection);
358
383
  });
384
+ commands.notifyCommandChanged(CommandIDs.toggleStoryPresentationMode);
359
385
  },
360
386
  });
361
387
  commands.addCommand(CommandIDs.renameGroup, {
@@ -831,7 +857,33 @@ export function addCommands(app, tracker, translator, formSchemaRegistry, layerB
831
857
  return;
832
858
  }
833
859
  current.model.addStorySegment();
860
+ commands.notifyCommandChanged(CommandIDs.toggleStoryPresentationMode);
834
861
  } }, icons.get(CommandIDs.addStorySegment)));
862
+ commands.addCommand(CommandIDs.toggleStoryPresentationMode, Object.assign({ label: trans.__('Toggle Story Presentation Mode'), isToggled: () => {
863
+ const current = tracker.currentWidget;
864
+ if (!current) {
865
+ return false;
866
+ }
867
+ const { storyMapPresentationMode } = current.model.getOptions();
868
+ return storyMapPresentationMode !== null && storyMapPresentationMode !== void 0 ? storyMapPresentationMode : false;
869
+ }, isEnabled: () => {
870
+ var _a, _b, _c;
871
+ const storySegments = (_b = (_a = tracker.currentWidget) === null || _a === void 0 ? void 0 : _a.model.getSelectedStory().story) === null || _b === void 0 ? void 0 : _b.storySegments;
872
+ if (((_c = tracker.currentWidget) === null || _c === void 0 ? void 0 : _c.model.jgisSettings.storyMapsDisabled) ||
873
+ !storySegments ||
874
+ storySegments.length < 1) {
875
+ return false;
876
+ }
877
+ return true;
878
+ }, execute: args => {
879
+ const current = tracker.currentWidget;
880
+ if (!current) {
881
+ return;
882
+ }
883
+ const currentOptions = current.model.getOptions();
884
+ current.model.setOptions(Object.assign(Object.assign({}, currentOptions), { storyMapPresentationMode: !currentOptions.storyMapPresentationMode }));
885
+ commands.notifyCommandChanged(CommandIDs.toggleStoryPresentationMode);
886
+ } }, icons.get(CommandIDs.toggleStoryPresentationMode)));
835
887
  loadKeybindings(commands, keybindings);
836
888
  }
837
889
  var Private;
package/lib/constants.js CHANGED
@@ -36,6 +36,9 @@ const iconObject = {
36
36
  [CommandIDs.temporalController]: { icon: clockIcon },
37
37
  [CommandIDs.addMarker]: { icon: markerIcon },
38
38
  [CommandIDs.addStorySegment]: { iconClass: 'fa fa-link' },
39
+ [CommandIDs.toggleStoryPresentationMode]: {
40
+ iconClass: 'fa fa-book jgis-icon-adjust',
41
+ },
39
42
  };
40
43
  /**
41
44
  * The registered icons
@@ -1,10 +1,4 @@
1
1
  import { IJGISLayer, IJupyterGISModel } from '@jupytergis/schema';
2
- export interface IBandHistogram {
3
- buckets: number[];
4
- count: number;
5
- max: number;
6
- min: number;
7
- }
8
2
  export interface IBandRow {
9
3
  band: number;
10
4
  colorInterpretation?: string;
@@ -10,7 +10,6 @@ const useGetBandInfo = (model, layer) => {
10
10
  setLoading(true);
11
11
  setError(null);
12
12
  try {
13
- const bandsArr = [];
14
13
  const source = model.getSource((_a = layer === null || layer === void 0 ? void 0 : layer.parameters) === null || _a === void 0 ? void 0 : _a.source);
15
14
  const sourceInfo = (_b = source === null || source === void 0 ? void 0 : source.parameters) === null || _b === void 0 ? void 0 : _b.urls[0];
16
15
  if (!(sourceInfo === null || sourceInfo === void 0 ? void 0 : sourceInfo.url)) {
@@ -40,9 +39,10 @@ const useGetBandInfo = (model, layer) => {
40
39
  }
41
40
  const image = await tiff.getImage();
42
41
  const numberOfBands = image.getSamplesPerPixel();
42
+ const bandsArr = [];
43
43
  for (let i = 0; i < numberOfBands; i++) {
44
44
  bandsArr.push({
45
- band: i,
45
+ band: i + 1,
46
46
  stats: {
47
47
  minimum: (_c = sourceInfo.min) !== null && _c !== void 0 ? _c : 0,
48
48
  maximum: (_d = sourceInfo.max) !== null && _d !== void 0 ? _d : 100,
@@ -87,9 +87,9 @@ const MultibandColor = ({ model, okSignalPromise, cancel, layerId, }) => {
87
87
  return (React.createElement("div", { className: "jp-gis-layer-symbology-container" },
88
88
  React.createElement("div", { className: "jp-gis-band-container" },
89
89
  React.createElement(LoadingOverlay, { loading: loading }),
90
- React.createElement(BandRow, { label: "Red Band", index: selectedBands.red - 1, bandRow: bandRows[selectedBands.red - 1], bandRows: bandRows, setSelectedBand: val => updateBand('red', val >= 0 ? val + 1 : 0), setBandRows: setBandRows, isMultibandColor: true }),
91
- React.createElement(BandRow, { label: "Green Band", index: selectedBands.green - 1, bandRow: bandRows[selectedBands.green - 1], bandRows: bandRows, setSelectedBand: val => updateBand('green', val >= 0 ? val + 1 : 0), setBandRows: setBandRows, isMultibandColor: true }),
92
- React.createElement(BandRow, { label: "Blue Band", index: selectedBands.blue - 1, bandRow: bandRows[selectedBands.blue - 1], bandRows: bandRows, setSelectedBand: val => updateBand('blue', val >= 0 ? val + 1 : 0), setBandRows: setBandRows, isMultibandColor: true }),
93
- React.createElement(BandRow, { label: "Alpha Band", index: selectedBands.alpha - 1, bandRow: bandRows[selectedBands.alpha - 1], bandRows: bandRows, setSelectedBand: val => updateBand('alpha', val >= 0 ? val + 1 : 0), setBandRows: setBandRows, isMultibandColor: true }))));
90
+ React.createElement(BandRow, { label: "Red Band", index: selectedBands.red - 1, bandRow: bandRows[selectedBands.red - 1], bandRows: bandRows, setSelectedBand: val => updateBand('red', val >= 0 ? val : 0), setBandRows: setBandRows, isMultibandColor: true }),
91
+ React.createElement(BandRow, { label: "Green Band", index: selectedBands.green - 1, bandRow: bandRows[selectedBands.green - 1], bandRows: bandRows, setSelectedBand: val => updateBand('green', val >= 0 ? val : 0), setBandRows: setBandRows, isMultibandColor: true }),
92
+ React.createElement(BandRow, { label: "Blue Band", index: selectedBands.blue - 1, bandRow: bandRows[selectedBands.blue - 1], bandRows: bandRows, setSelectedBand: val => updateBand('blue', val >= 0 ? val : 0), setBandRows: setBandRows, isMultibandColor: true }),
93
+ React.createElement(BandRow, { label: "Alpha Band", index: selectedBands.alpha - 1, bandRow: bandRows[selectedBands.alpha - 1], bandRows: bandRows, setSelectedBand: val => updateBand('alpha', val >= 0 ? val : 0), setBandRows: setBandRows, isMultibandColor: true }))));
94
94
  };
95
95
  export default MultibandColor;
@@ -1,8 +1,9 @@
1
1
  import { IDict } from '@jupytergis/schema';
2
+ import { RJSFSchema, UiSchema } from '@rjsf/utils';
2
3
  import { BaseForm } from './baseform';
3
4
  /**
4
- * The form to modify a hillshade layer.
5
+ * The form to modify story map properties.
5
6
  */
6
7
  export declare class StoryEditorPropertiesForm extends BaseForm {
7
- protected processSchema(data: IDict<any> | undefined, schema: IDict, uiSchema: IDict): void;
8
+ protected processSchema(data: IDict<any> | undefined, schema: RJSFSchema, uiSchema: UiSchema): void;
8
9
  }
@@ -1,10 +1,16 @@
1
1
  import { BaseForm } from './baseform';
2
2
  /**
3
- * The form to modify a hillshade layer.
3
+ * The form to modify story map properties.
4
4
  */
5
5
  export class StoryEditorPropertiesForm extends BaseForm {
6
6
  processSchema(data, schema, uiSchema) {
7
7
  super.processSchema(data, schema, uiSchema);
8
8
  this.removeFormEntry('storySegments', data, schema, uiSchema);
9
+ uiSchema.presentaionBgColor = {
10
+ 'ui:widget': 'color',
11
+ };
12
+ uiSchema.presentaionTextColor = {
13
+ 'ui:widget': 'color',
14
+ };
9
15
  }
10
16
  }
@@ -31,10 +31,12 @@ interface IStates {
31
31
  }>;
32
32
  displayTemporalController: boolean;
33
33
  filterStates: IDict<IJGISFilterItem | undefined>;
34
+ isSpectaPresentation: boolean;
34
35
  }
35
36
  export declare class MainView extends React.Component<IProps, IStates> {
36
37
  constructor(props: IProps);
37
38
  componentDidMount(): Promise<void>;
39
+ componentDidUpdate(prevProps: IProps, prevState: IStates): void;
38
40
  componentWillUnmount(): void;
39
41
  generateMap(center: number[], zoom: number): Promise<void>;
40
42
  updateCenter: () => void;
@@ -134,6 +136,7 @@ export declare class MainView extends React.Component<IProps, IStates> {
134
136
  removeLayer(id: string): void;
135
137
  private _onClientSharedStateChanged;
136
138
  private _onSharedOptionsChanged;
139
+ private _onSettingsChanged;
137
140
  private updateOptions;
138
141
  private _onViewChanged;
139
142
  /**
@@ -167,6 +170,15 @@ export declare class MainView extends React.Component<IProps, IStates> {
167
170
  private _onLayerTreeChange;
168
171
  private _onSourcesChange;
169
172
  private _onSharedModelStateChange;
173
+ /**
174
+ * Handler for when story maps change in the model.
175
+ * Updates specta state and presentation colors when story data becomes available.
176
+ */
177
+ private _setupSpectaMode;
178
+ private _removeAllInteractions;
179
+ private _setupStoryScrollListener;
180
+ private _cleanupStoryScrollListener;
181
+ private _updateSpectaPresentationColors;
170
182
  private _onSharedMetadataChanged;
171
183
  private _computeAnnotationPosition;
172
184
  private _updateAnnotation;
@@ -187,7 +199,11 @@ export declare class MainView extends React.Component<IProps, IStates> {
187
199
  private _commands;
188
200
  private _isPositionInitialized;
189
201
  private divRef;
202
+ private controlsToolbarRef;
203
+ private spectaContainerRef;
204
+ private storyViewerPanelRef;
190
205
  private _Map;
206
+ private _zoomControl?;
191
207
  private _model;
192
208
  private _mainViewModel;
193
209
  private _ready;
@@ -203,5 +219,7 @@ export declare class MainView extends React.Component<IProps, IStates> {
203
219
  private _formSchemaRegistry?;
204
220
  private _annotationModel?;
205
221
  private _featurePropertyCache;
222
+ private _isSpectaPresentationInitialized;
223
+ private _storyScrollHandler;
206
224
  }
207
225
  export {};
@@ -16,12 +16,12 @@ import { UUID } from '@lumino/coreutils';
16
16
  import { ContextMenu } from '@lumino/widgets';
17
17
  import { Collection, Map as OlMap, View, getUid, } from 'ol';
18
18
  import Feature from 'ol/Feature';
19
- import { FullScreen, ScaleLine } from 'ol/control';
19
+ import { FullScreen, ScaleLine, Zoom } from 'ol/control';
20
20
  import { singleClick } from 'ol/events/condition';
21
21
  import { getCenter } from 'ol/extent';
22
22
  import { GeoJSON, MVT } from 'ol/format';
23
23
  import { Point } from 'ol/geom';
24
- import { DragAndDrop, Select } from 'ol/interaction';
24
+ import { DragAndDrop, DragPan, DragRotate, DragZoom, KeyboardPan, KeyboardZoom, MouseWheelZoom, PinchRotate, PinchZoom, DoubleClickZoom, Select, } from 'ol/interaction';
25
25
  import { Heatmap as HeatmapLayer, Image as ImageLayer, Layer, Vector as VectorLayer, VectorTile as VectorTileLayer, WebGLTile as WebGlTileLayer, } from 'ol/layer';
26
26
  import TileLayer from 'ol/layer/Tile';
27
27
  import { fromLonLat, get as getProjection, toLonLat, transformExtent, } from 'ol/proj';
@@ -44,8 +44,10 @@ import { debounce, isLightTheme, loadFile, throttle } from "../tools";
44
44
  import CollaboratorPointers from './CollaboratorPointers';
45
45
  import { FollowIndicator } from './FollowIndicator';
46
46
  import TemporalSlider from './TemporalSlider';
47
+ import { hexToRgb } from '../dialogs/symbology/colorRampUtils';
47
48
  import { markerIcon } from '../icons';
48
49
  import { LeftPanel, RightPanel } from '../panelview';
50
+ import StoryViewerPanel from '../panelview/story-maps/StoryViewerPanel';
49
51
  export class MainView extends React.Component {
50
52
  constructor(props) {
51
53
  super(props);
@@ -406,6 +408,138 @@ export class MainView extends React.Component {
406
408
  }
407
409
  }
408
410
  };
411
+ /**
412
+ * Handler for when story maps change in the model.
413
+ * Updates specta state and presentation colors when story data becomes available.
414
+ */
415
+ this._setupSpectaMode = () => {
416
+ this._removeAllInteractions();
417
+ this._setupStoryScrollListener();
418
+ // Update colors CSS variables with colors from story
419
+ this._updateSpectaPresentationColors();
420
+ };
421
+ this._removeAllInteractions = () => {
422
+ // Remove all default interactions
423
+ const interactions = this._Map.getInteractions();
424
+ const interactionArray = interactions.getArray();
425
+ // Remove each interaction type
426
+ const interactionsToRemove = [
427
+ DragPan,
428
+ DragRotate,
429
+ DragZoom,
430
+ KeyboardPan,
431
+ KeyboardZoom,
432
+ MouseWheelZoom,
433
+ PinchRotate,
434
+ PinchZoom,
435
+ DoubleClickZoom,
436
+ DragAndDrop,
437
+ Select,
438
+ ];
439
+ this._zoomControl && this._Map.removeControl(this._zoomControl);
440
+ interactionsToRemove.forEach(InteractionClass => {
441
+ const interaction = interactionArray.find(interaction => interaction instanceof InteractionClass);
442
+ if (interaction) {
443
+ this._Map.removeInteraction(interaction);
444
+ }
445
+ });
446
+ };
447
+ this._setupStoryScrollListener = () => {
448
+ const segmentNavigationThrottle = 750; // Minimum time between segment changes (ms)
449
+ const SCROLL_EDGE_THRESHOLD = 0; // Pixels from top/bottom to trigger segment change
450
+ // Create throttled functions that call the current panel handle dynamically
451
+ const throttledHandleNext = throttle(() => {
452
+ const panelHandle = this.storyViewerPanelRef.current;
453
+ panelHandle === null || panelHandle === void 0 ? void 0 : panelHandle.handleNext();
454
+ }, segmentNavigationThrottle);
455
+ const throttledHandlePrev = throttle(() => {
456
+ const panelHandle = this.storyViewerPanelRef.current;
457
+ panelHandle === null || panelHandle === void 0 ? void 0 : panelHandle.handlePrev();
458
+ }, segmentNavigationThrottle);
459
+ const handleScroll = (e) => {
460
+ const currentPanelHandle = this.storyViewerPanelRef.current;
461
+ if (!currentPanelHandle || !currentPanelHandle.canNavigate) {
462
+ return;
463
+ }
464
+ const wheelEvent = e;
465
+ const target = wheelEvent.target;
466
+ // Find the story viewer panel
467
+ const storyViewerPanel = document.querySelector('.jgis-story-viewer-panel');
468
+ // If no panel found, change segments normally
469
+ if (!storyViewerPanel) {
470
+ wheelEvent.preventDefault();
471
+ wheelEvent.deltaY > 0 ? throttledHandleNext() : throttledHandlePrev();
472
+ return;
473
+ }
474
+ const hasOverflow = storyViewerPanel.scrollHeight > storyViewerPanel.clientHeight;
475
+ // If panel has no overflow, change segments normally
476
+ if (!hasOverflow) {
477
+ wheelEvent.preventDefault();
478
+ wheelEvent.deltaY > 0 ? throttledHandleNext() : throttledHandlePrev();
479
+ return;
480
+ }
481
+ // Panel has overflow - handle scroll forwarding and edge detection
482
+ const scrollTop = storyViewerPanel.scrollTop;
483
+ const scrollHeight = storyViewerPanel.scrollHeight;
484
+ const clientHeight = storyViewerPanel.clientHeight;
485
+ const isAtBottom = scrollTop + clientHeight >= scrollHeight - SCROLL_EDGE_THRESHOLD;
486
+ const isAtTop = scrollTop <= SCROLL_EDGE_THRESHOLD;
487
+ const isScrollingDown = wheelEvent.deltaY > 0;
488
+ const isScrollingUp = wheelEvent.deltaY < 0;
489
+ // At edges: change segments
490
+ if ((isScrollingDown && isAtBottom) || (isScrollingUp && isAtTop)) {
491
+ wheelEvent.preventDefault();
492
+ isScrollingDown ? throttledHandleNext() : throttledHandlePrev();
493
+ return;
494
+ }
495
+ // If scrolling inside the panel, let it scroll naturally
496
+ if (target.closest('.jgis-story-viewer-panel')) {
497
+ return;
498
+ }
499
+ // Scrolling outside the panel: forward scroll to panel (no throttling for smooth scrolling)
500
+ wheelEvent.preventDefault();
501
+ const newScrollTop = Math.max(0, Math.min(scrollHeight - clientHeight, scrollTop + wheelEvent.deltaY));
502
+ storyViewerPanel.scrollTop = newScrollTop;
503
+ };
504
+ this._storyScrollHandler = handleScroll;
505
+ // Attach wheel event listener to the main container
506
+ const containerElement = document.querySelector('.jGIS-Mainview-Container');
507
+ if (containerElement) {
508
+ containerElement.addEventListener('wheel', handleScroll, {
509
+ passive: false,
510
+ });
511
+ }
512
+ };
513
+ this._cleanupStoryScrollListener = () => {
514
+ if (this._storyScrollHandler) {
515
+ const containerElement = document.querySelector('.jGIS-Mainview-Container');
516
+ if (containerElement) {
517
+ containerElement.removeEventListener('wheel', this._storyScrollHandler);
518
+ }
519
+ this._storyScrollHandler = null;
520
+ }
521
+ };
522
+ this._updateSpectaPresentationColors = () => {
523
+ var _a;
524
+ // Try ref first, fallback to querySelector if ref not available yet
525
+ const container = this.spectaContainerRef.current ||
526
+ ((_a = this.divRef.current) === null || _a === void 0 ? void 0 : _a.querySelector('.jgis-specta-story-panel-container'));
527
+ if (!container) {
528
+ return;
529
+ }
530
+ const story = this._model.getSelectedStory().story;
531
+ const bgColor = story === null || story === void 0 ? void 0 : story.presentaionBgColor;
532
+ const textColor = story === null || story === void 0 ? void 0 : story.presentaionTextColor;
533
+ // Set background color
534
+ if (bgColor) {
535
+ const rgb = hexToRgb(bgColor);
536
+ container.style.setProperty('--jgis-specta-bg-color', `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`);
537
+ }
538
+ // Set text color
539
+ if (textColor) {
540
+ container.style.setProperty('--jgis-specta-text-color', textColor);
541
+ }
542
+ };
409
543
  this._onSharedMetadataChanged = (_, changes) => {
410
544
  const newState = Object.assign({}, this.state.annotations);
411
545
  changes.forEach((val, key) => {
@@ -451,10 +585,15 @@ export class MainView extends React.Component {
451
585
  };
452
586
  this._isPositionInitialized = false;
453
587
  this.divRef = React.createRef(); // Reference of render div
588
+ this.controlsToolbarRef = React.createRef();
589
+ this.spectaContainerRef = React.createRef();
590
+ this.storyViewerPanelRef = React.createRef();
454
591
  this._ready = false;
455
592
  this._sourceToLayerMap = new Map();
456
593
  this._originalFeatures = {};
457
594
  this._featurePropertyCache = new Map();
595
+ this._isSpectaPresentationInitialized = false;
596
+ this._storyScrollHandler = null;
458
597
  this._state = props.state;
459
598
  this._formSchemaRegistry = props.formSchemaRegistry;
460
599
  this._annotationModel = props.annotationModel;
@@ -489,6 +628,7 @@ export class MainView extends React.Component {
489
628
  this._model.sharedModel.changed.connect(this._onSharedModelStateChange);
490
629
  this._model.sharedMetadataChanged.connect(this._onSharedMetadataChanged, this);
491
630
  this._model.zoomToPositionSignal.connect(this._onZoomToPosition, this);
631
+ this._model.settingsChanged.connect(this._onSettingsChanged, this);
492
632
  this._model.updateLayerSignal.connect(this._triggerLayerUpdate, this);
493
633
  this._model.addFeatureAsMsSignal.connect(this._convertFeatureToMs, this);
494
634
  this._model.geolocationChanged.connect(this._handleGeolocationChanged, this);
@@ -513,6 +653,7 @@ export class MainView extends React.Component {
513
653
  loadingErrors: [],
514
654
  displayTemporalController: false,
515
655
  filterStates: {},
656
+ isSpectaPresentation: false,
516
657
  };
517
658
  this._sources = [];
518
659
  this._loadingLayers = new Set();
@@ -530,12 +671,19 @@ export class MainView extends React.Component {
530
671
  : [0, 0];
531
672
  const zoom = options.zoom !== undefined ? options.zoom : 1;
532
673
  await this.generateMap(center, zoom);
533
- this.addContextMenu();
534
674
  this._mainViewModel.initSignal();
535
675
  if (window.jupytergisMaps !== undefined && this._documentPath) {
536
676
  window.jupytergisMaps[this._documentPath] = this._Map;
537
677
  }
538
678
  }
679
+ componentDidUpdate(prevProps, prevState) {
680
+ // Run setup when isSpectaPresentation changes from false/undefined to true
681
+ if (this.state.isSpectaPresentation &&
682
+ !this._isSpectaPresentationInitialized) {
683
+ this._setupSpectaMode();
684
+ this._isSpectaPresentationInitialized = true;
685
+ }
686
+ }
539
687
  componentWillUnmount() {
540
688
  if (window.jupytergisMaps !== undefined && this._documentPath) {
541
689
  delete window.jupytergisMaps[this._documentPath];
@@ -543,20 +691,37 @@ export class MainView extends React.Component {
543
691
  window.removeEventListener('resize', this._handleWindowResize);
544
692
  this._mainViewModel.viewSettingChanged.disconnect(this._onViewChanged, this);
545
693
  this._model.themeChanged.disconnect(this._handleThemeChange, this);
694
+ this._model.settingsChanged.disconnect(this._onSettingsChanged, this);
546
695
  this._model.sharedOptionsChanged.disconnect(this._onSharedOptionsChanged, this);
547
696
  this._model.clientStateChanged.disconnect(this._onClientSharedStateChanged, this);
697
+ // Clean up story scroll listener
698
+ this._cleanupStoryScrollListener();
548
699
  this._mainViewModel.dispose();
549
700
  }
550
701
  async generateMap(center, zoom) {
702
+ const scaleLine = new ScaleLine({
703
+ target: this.controlsToolbarRef.current || undefined,
704
+ });
705
+ const fullScreen = new FullScreen({
706
+ target: this.controlsToolbarRef.current || undefined,
707
+ });
708
+ this._zoomControl = new Zoom({
709
+ target: this.controlsToolbarRef.current || undefined,
710
+ });
711
+ const controls = [scaleLine, fullScreen];
712
+ if (this._model.jgisSettings.zoomButtonsEnabled) {
713
+ controls.push(this._zoomControl);
714
+ }
551
715
  if (this.divRef.current) {
552
716
  this._Map = new OlMap({
553
717
  target: this.divRef.current,
718
+ keyboardEventTarget: document,
554
719
  layers: [],
555
720
  view: new View({
556
721
  center,
557
722
  zoom,
558
723
  }),
559
- controls: [new ScaleLine(), new FullScreen()],
724
+ controls,
560
725
  });
561
726
  // Add map interactions
562
727
  const dragAndDropInteraction = new DragAndDrop({
@@ -1379,12 +1544,42 @@ export class MainView extends React.Component {
1379
1544
  }
1380
1545
  }
1381
1546
  _onSharedOptionsChanged() {
1547
+ // ! would prefer a model ready signal or something, this feels hacky
1548
+ const enableSpectaPresentation = this._model.isSpectaMode();
1549
+ // Handle initialization based on specta presentation state
1550
+ if (!this._isSpectaPresentationInitialized) {
1551
+ if (enableSpectaPresentation) {
1552
+ // _setupSpectaMode will be called in componentDidUpdate when state changes
1553
+ this.setState(old => (Object.assign(Object.assign({}, old), { isSpectaPresentation: true })));
1554
+ }
1555
+ else {
1556
+ // Add context menu when not in specta mode
1557
+ this.addContextMenu();
1558
+ this._isSpectaPresentationInitialized = true;
1559
+ }
1560
+ }
1382
1561
  if (!this._isPositionInitialized) {
1383
1562
  const options = this._model.getOptions();
1384
1563
  this.updateOptions(options);
1385
1564
  this._isPositionInitialized = true;
1386
1565
  }
1387
1566
  }
1567
+ _onSettingsChanged(sender, key) {
1568
+ if (key !== 'zoomButtonsEnabled' || !this._Map) {
1569
+ return;
1570
+ }
1571
+ const enabled = this._model.jgisSettings.zoomButtonsEnabled;
1572
+ if (!enabled && this._zoomControl) {
1573
+ this._Map.removeControl(this._zoomControl);
1574
+ this._zoomControl = undefined;
1575
+ }
1576
+ if (enabled && !this._zoomControl) {
1577
+ this._zoomControl = new Zoom({
1578
+ target: this.controlsToolbarRef.current || undefined,
1579
+ });
1580
+ this._Map.addControl(this._zoomControl);
1581
+ }
1582
+ }
1388
1583
  async updateOptions(options) {
1389
1584
  const { projection, extent, useExtent, latitude, longitude, zoom, bearing, } = options;
1390
1585
  let view = this._Map.getView();
@@ -1555,7 +1750,7 @@ export class MainView extends React.Component {
1555
1750
  }
1556
1751
  // TODO this and flyToPosition need a rework
1557
1752
  _onZoomToPosition(_, id) {
1558
- var _a, _b;
1753
+ var _a, _b, _c;
1559
1754
  // Check if the id is an annotation
1560
1755
  const annotation = (_a = this._model.annotationModel) === null || _a === void 0 ? void 0 : _a.getAnnotation(id);
1561
1756
  if (annotation) {
@@ -1566,17 +1761,44 @@ export class MainView extends React.Component {
1566
1761
  let extent;
1567
1762
  const layer = this.getLayer(id);
1568
1763
  const source = layer === null || layer === void 0 ? void 0 : layer.getSource();
1569
- // TODO: Story segment layers don't have an associated OL layer
1570
- // This could be better
1764
+ const jgisLayer = this._model.getLayer(id);
1765
+ /**
1766
+ * Layer may be undefined in two cases:
1767
+ * 1. StorySegmentLayer: These layers don't have an associated OpenLayers layer
1768
+ * 2. StacLayer: When centerOnPosition is called immediately after adding the layer,
1769
+ * the OpenLayers layer hasn't been created yet, so we use the bbox from the
1770
+ * layer model's STAC data directly.
1771
+ */
1571
1772
  if (!layer) {
1572
- const jgisLayer = this._model.getLayer(id);
1573
- const layerParams = jgisLayer === null || jgisLayer === void 0 ? void 0 : jgisLayer.parameters;
1574
- const coords = getCenter(layerParams.extent);
1575
- // TODO: Should pass args through signal??
1576
- // const { story } = this._model.getSelectedStory();
1577
- this._flyToPosition({ x: coords[0], y: coords[1] }, layerParams.zoom, ((_b = layerParams.transition.time) !== null && _b !== void 0 ? _b : 1) * 1000, // seconds -> ms
1578
- layerParams.transition.type);
1579
- return;
1773
+ // Handle StacLayer that hasn't been added to the map yet
1774
+ if ((jgisLayer === null || jgisLayer === void 0 ? void 0 : jgisLayer.type) === 'StacLayer') {
1775
+ const layerParams = jgisLayer.parameters;
1776
+ const stacBbox = (_b = layerParams.data) === null || _b === void 0 ? void 0 : _b.bbox;
1777
+ if (stacBbox && stacBbox.length === 4) {
1778
+ // STAC bbox format: [west, south, east, north] in EPSG:4326
1779
+ const [west, south, east, north] = stacBbox;
1780
+ const bboxExtent = [west, south, east, north];
1781
+ // Convert from EPSG:4326 to view projection
1782
+ const viewProjection = this._Map.getView().getProjection();
1783
+ const transformedExtent = viewProjection.getCode() !== 'EPSG:4326'
1784
+ ? transformExtent(bboxExtent, 'EPSG:4326', viewProjection)
1785
+ : bboxExtent;
1786
+ this._Map.getView().fit(transformedExtent, {
1787
+ size: this._Map.getSize(),
1788
+ duration: 500,
1789
+ padding: [250, 250, 250, 250],
1790
+ });
1791
+ return;
1792
+ }
1793
+ }
1794
+ // Handle StorySegmentLayer
1795
+ if ((jgisLayer === null || jgisLayer === void 0 ? void 0 : jgisLayer.type) === 'StorySegmentLayer') {
1796
+ const layerParams = jgisLayer.parameters;
1797
+ const coords = getCenter(layerParams.extent);
1798
+ this._flyToPosition({ x: coords[0], y: coords[1] }, layerParams.zoom, ((_c = layerParams.transition.time) !== null && _c !== void 0 ? _c : 1) * 1000, // seconds -> ms
1799
+ layerParams.transition.type);
1800
+ return;
1801
+ }
1580
1802
  }
1581
1803
  if (source instanceof VectorSource) {
1582
1804
  extent = source.getExtent();
@@ -1829,7 +2051,7 @@ export class MainView extends React.Component {
1829
2051
  }),
1830
2052
  React.createElement("div", { className: "jGIS-Mainview-Container" },
1831
2053
  this.state.displayTemporalController && (React.createElement(TemporalSlider, { model: this._model, filterStates: this.state.filterStates })),
1832
- React.createElement("div", { className: "jGIS-Mainview data-jgis-keybinding", tabIndex: -2, style: {
2054
+ React.createElement("div", { className: "jGIS-Mainview data-jgis-keybinding", tabIndex: 0, style: {
1833
2055
  border: this.state.remoteUser
1834
2056
  ? `solid 3px ${this.state.remoteUser.color}`
1835
2057
  : 'unset',
@@ -1841,9 +2063,12 @@ export class MainView extends React.Component {
1841
2063
  width: '100%',
1842
2064
  height: '100%',
1843
2065
  } },
1844
- React.createElement("div", { className: "jgis-panels-wrapper" },
2066
+ React.createElement("div", { className: "jgis-panels-wrapper" }, !this.state.isSpectaPresentation ? (React.createElement(React.Fragment, null,
1845
2067
  this._state && (React.createElement(LeftPanel, { model: this._model, commands: this._mainViewModel.commands, state: this._state })),
1846
- this._formSchemaRegistry && this._annotationModel && (React.createElement(RightPanel, { model: this._model, formSchemaRegistry: this._formSchemaRegistry, annotationModel: this._annotationModel }))))),
2068
+ this._formSchemaRegistry && this._annotationModel && (React.createElement(RightPanel, { model: this._model, commands: this._mainViewModel.commands, formSchemaRegistry: this._formSchemaRegistry, annotationModel: this._annotationModel })))) : (React.createElement("div", { className: "jgis-specta-right-panel-container-mod jgis-right-panel-container" },
2069
+ React.createElement("div", { ref: this.spectaContainerRef, className: "jgis-specta-story-panel-container" },
2070
+ React.createElement(StoryViewerPanel, { ref: this.storyViewerPanelRef, model: this._model, isSpecta: this.state.isSpectaPresentation }))))),
2071
+ React.createElement("div", { ref: this.controlsToolbarRef, className: "jgis-controls-toolbar" }))),
1847
2072
  React.createElement(StatusBar, { jgisModel: this._model, loading: this.state.loadingLayer, projection: this.state.viewProjection, scale: this.state.scale }))));
1848
2073
  }
1849
2074
  }
@@ -1,7 +1,7 @@
1
1
  import { Button } from '@jupyterlab/ui-components';
2
2
  import { cloneDeep } from 'lodash';
3
3
  import React, { useEffect, useRef, useState } from 'react';
4
- import { debounce, loadFile } from "../../../tools";
4
+ import { debounce, loadFile } from "../../tools";
5
5
  import FilterRow from './FilterRow';
6
6
  const FilterComponent = ({ model }) => {
7
7
  const featuresInLayerRef = useRef({});